|
| 1 | +- Start Date: 2019-05-30 |
| 2 | +- Relevant Team(s): Ember.js, Learning |
| 3 | +- RFC PR: https://github.com/emberjs/rfcs/pull/494 |
| 4 | +- Tracking: (leave this empty) |
| 5 | + |
| 6 | +# Async Observers |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +Add a way to specify whether or not observers should fire synchronously - |
| 11 | +that is, immediately after the property they are observing has changed - or |
| 12 | +asynchronously during the next runloop, along with an optional feature to |
| 13 | +specify whether observers should default to sync or async. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +Observers have been run synchronously in Ember since before v1.0 was released, |
| 18 | +and for about as long it has been an intention of the core team to eventually |
| 19 | +make them asynchronous. There are a two main reasons for why triggering |
| 20 | +observers asynchronously would be better overall: |
| 21 | + |
| 22 | +- They promote better programming practices. Synchronous observers can be used |
| 23 | + in a lot of ways to interact with the code they are observing, which puts more |
| 24 | + code on the "hot-path" and is prone to create a mess of intertangled, loosely |
| 25 | + related code filled with [spooky action at a distance](<https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)>). |
| 26 | +- It would allow us to clean up a significant chunk of code within Ember |
| 27 | + itself. There is non-trivial amount of code dedicated to sending change |
| 28 | + signals synchronously, and that code has been slowly replaced by an |
| 29 | + alternative system that is lazy. Asynchronous observers would allow us to |
| 30 | + remove legacy code and tech debt. |
| 31 | + |
| 32 | +We implemented this change behind a feature flag, and several community members |
| 33 | +tested it out in their applications. In testing, we found that this was |
| 34 | +unfortunately too much of a breaking change to do all at once - like it or not, |
| 35 | +the timing semantics of observers are public API. |
| 36 | + |
| 37 | +The proposed solution now is to provide a method for users to specify whether an |
| 38 | +observer should be sync or async. In existing apps, observers can be converted |
| 39 | +incrementally to be async, giving them a path forward. In addition, an optional |
| 40 | +feature will be made which sets observers to be async by default, allowing users |
| 41 | +to set the default once their whole app has been converted, and allowing new |
| 42 | +apps to prevent/discourage sync observers in the first place. In the long run, |
| 43 | +synchronous observers will be deprecated and removed. |
| 44 | + |
| 45 | +## Detailed design |
| 46 | + |
| 47 | +### New APIs |
| 48 | + |
| 49 | +A new `sync` boolean argument will be added to both `addObserver` and |
| 50 | +`removeObserver`: |
| 51 | + |
| 52 | +```ts |
| 53 | +export function addObserver( |
| 54 | + obj: any, |
| 55 | + path: string, |
| 56 | + target: object | Function | null, |
| 57 | + method?: string | Function, |
| 58 | + sync = SYNC_DEFAULT |
| 59 | +): void; |
| 60 | + |
| 61 | +export function removeObserver( |
| 62 | + obj: any, |
| 63 | + path: string, |
| 64 | + target: object | Function | null, |
| 65 | + method?: string | Function, |
| 66 | + sync = SYNC_DEFAULT |
| 67 | +): void; |
| 68 | +``` |
| 69 | + |
| 70 | +The argument needs to be added to both because sync and async observers are |
| 71 | +tracked separately, so we need to know where to look for the observer when |
| 72 | +removing it. Attempting to add both a sync and async observer will throw an |
| 73 | +error. |
| 74 | + |
| 75 | +In addition, a new overloaded form of `observer` will allow users to specify |
| 76 | +whether or not the observer should be sync or async: |
| 77 | + |
| 78 | +```ts |
| 79 | +type ObserverDefinition = { |
| 80 | + dependentKeys: string[]; |
| 81 | + fn: Function; |
| 82 | + sync: boolean; |
| 83 | +}; |
| 84 | + |
| 85 | +export function observer(...args: (string | Function)[]): Function; |
| 86 | +export function observer(definition: ObserverDefinition): Function; |
| 87 | +``` |
| 88 | + |
| 89 | +Users will have to provide a full `ObserverDefinition` to set `sync`, which will |
| 90 | +prevent us from having to do any more argument munging to figure out what the |
| 91 | +user wants. |
| 92 | + |
| 93 | +### Synchronous Observer Implementation |
| 94 | + |
| 95 | +Since chains are removed, the only way to check if observers should fire is to |
| 96 | +cycle through all of them. This means that on every `notifyPropertyChange`, we |
| 97 | +will cycle through _all_ active synchronous observers and fire any that have |
| 98 | +dirtied. |
| 99 | + |
| 100 | +In apps that are observer heavy, this could lead to performance impacts. |
| 101 | +Unfortunately, there isn't much we can do about this. We will try to minimize |
| 102 | +the impact as much as possible, but in the end it will be up to individual |
| 103 | +applications to migrate away from synchronous observers over time. |
| 104 | + |
| 105 | +### Tracked Properties and `@dependentKeyCompat` |
| 106 | + |
| 107 | +Tracked properties and `@dependentKeyCompat` marked getters/setters will _not_ |
| 108 | +fire observers synchronously, since they do not use `notifyPropertyChange` or |
| 109 | +the old change tracking system at all. In this way, they will encourage users to |
| 110 | +convert to async observers, or away from observers entirely. |
| 111 | + |
| 112 | +### Optional Feature |
| 113 | + |
| 114 | +The name of the feature will be `default-async-observers`. Enabling it will |
| 115 | +default all observers to be async, but still allow users to set observers to be |
| 116 | +synchronous manually. This flag will be enabled by default in Ember Octane. |
| 117 | + |
| 118 | +## How we teach this |
| 119 | + |
| 120 | +### API Docs |
| 121 | + |
| 122 | +(To be added at the [end of the current API docs](https://github.com/emberjs/ember.js/blob/4a98e1610b795edb544513f10a8870af1375141d/packages/%40ember/-internals/runtime/lib/mixins/observable.js#L359)) |
| 123 | + |
| 124 | +#### `sync` |
| 125 | + |
| 126 | +By default in new Ember applications, observers are asynchronous. They can be |
| 127 | +marked as _synchronous_ instead by using the `sync` option. Synchronous |
| 128 | +observers will run immediately when the property they are observing changes, |
| 129 | +instead of being scheduled to run later. |
| 130 | + |
| 131 | +Each synchronous observer has a performance impact for every property change, so |
| 132 | +you should generally avoid using synchronous observers. |
| 133 | + |
| 134 | +In older applications, observers are synchronous by default. You can use the |
| 135 | +`sync` option to make them asynchronous instead and convert them over time. You |
| 136 | +can also enable the `default-async-observers` optional feature to make them |
| 137 | +asynchronous by default, once you are sure that they will continue to function |
| 138 | +if they are asynchronous. |
| 139 | + |
| 140 | +### Guides |
| 141 | + |
| 142 | +Observers are not discussed in the post-Octane guides, since we don't want to |
| 143 | +encourage their use. It may make sense to include a section on them in the |
| 144 | +upgrade guide instead. |
| 145 | + |
| 146 | +### Upgrade Guides |
| 147 | + |
| 148 | +We should make a note in the Octane upgrade guides that sync observers are |
| 149 | +discouraged and probably not very performant. We should be up front that this |
| 150 | +will likely be a performance hit, but that we decided it was worth the benefits |
| 151 | +of the change. |
| 152 | + |
| 153 | +## Drawbacks |
| 154 | + |
| 155 | +The biggest potential drawback is in performance. While we haven't been able to |
| 156 | +do any testing on apps that have observers, its possible that these changes will |
| 157 | +have an impact on them, especially apps that have many observers. |
| 158 | + |
| 159 | +In theory, this shouldn't impact the majority of Ember apps since observers have |
| 160 | +been discouraged so heavily for such a long time. The impact should also |
| 161 | +decrease in time, as users transition away from observers entirely and toward |
| 162 | +tracked properties. |
| 163 | + |
| 164 | +## Alternatives |
| 165 | + |
| 166 | +- We could release Ember v4, and ship asynchronous observers as a breaking |
| 167 | + change. We currently believe this would be a breaking change that would |
| 168 | + prevent many users from adopting Octane or transitioning forward to tracked |
| 169 | + properties, which would be problematic and could divide the community. |
| 170 | + |
| 171 | +## Unresolved questions |
| 172 | + |
| 173 | +What is the exact performance impact? Can we test it out in an application that |
| 174 | +represents a typical Ember app that uses observers? |
0 commit comments