-
-
Notifications
You must be signed in to change notification settings - Fork 147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] synchronous vs asynchronous binding system for Aurelia 2 #1957
Comments
Firstly, @bigopon, incredible write-up. Loved the examples of other frameworks/libs and explanation of the state-tearing problem. From my perspective, the sync binding mode is a bit problematic, but once you know about it, it's easy to work around using something like Understandably, concessions had to be made, and we wanted to avoid some of the issues we had with Aurelia 1. Right now, I think this is more of a documentation/awareness issue than a fundamental problem with Aurelia 2 itself. I don't want us to delay the release of candidates because of this. Fundamentally, there is nothing major here that needs to be fixed immediately. If synchronous binding updates were causing other issues, I would be more inclined to say we should bite the bullet and look into supporting async. Not wanting to speak for everyone here, I think what we should do in the interim is:
I am not married to any particular approach here. I am used to the Aurelia 1 way of updating because I have worked with it for as long as others I know. It was acknowledged in v1 that if you had issues around bindings, you would throw In the long term, we could always cut another major release if we decide to change it. We aren't stuck with these decisions. But I do think it's more destructive in the short term to delay a stable Aurelia 2 release because this is holding up wider adoption from other libraries and tooling (like official Storybook support, etc.). My concerns and questions here relate to performance. Is the nature of sync that things are more predictable, or does one approach provide performance benefits over the other? Because I would support whatever approach results in better performance and fewer side effects over an approach that has the potential to slow my app down as the number of bindings increases. |
Probably, as an option... The binding should be a part of the transaction. The transaction should satisfy ACID rules. So,... I can't find too many use cases, where it may serve us. |
There won't be any difference we can tell, apart from the fact that they are processed 1 tick different.
Bindings are part of the transaction atm, and they'll behave like a computed example above. |
I would think any user-initiated event should auto batch imo. I am not sure of the consequences of this behavior but seems like the correct thing to do. 馃し |
one consequences of this behavior is user initiated event would run code differently, say |
Thanks for this comment, I've had some time to think about it more. In a change notification "tree", binding is like a leaf node, a computed one, but not computed getter. A binding won't be able to notify its dirty state to the property/whatever it's targeting. Consider the following example: <child full.bind="first + last"> The binding created from We can flip the perspective, and turn binding into a computed getter, together with making |
I'am having two major issues with current implementation.
class OuterComponent {
static template = '<inner-component prop1.bind="prop1" prop2.bind="prop2"/>';
@observable
prop1 = 1;
prop2 = 1;
attached() {
this.prop1 = 2;
}
prop1Changed() {
this.prop2 = 2;
}
}
class InnerComponent {
@bindable
prop1;
@bindable
prop2;
prop2Changed() {
console.log('prop1:', this.prop1);
}
} This example will output "prop1: 1" in the console. So when prop2 changes inside InnerComponent - prop1 still has the old value. That's very unexpected considering synchronous binding because I first changed prop1 value, then it's change callback fired, which in term changed prop2; So I would expect prop1 to be already updated inside InnerComponent.
|
Thanks. This is a good example, I think it's quite convincing for the need of reconsidering the callback timing
There used to be doc for this I believe, though somehow it's gone. It's |
I know about propertyChanged, but that's not what I meant. I meat exactly propertiesChanged(), which might have the following definition: function propertiesChanged(changes: Record<string, { name: string, newValue: unknown, oldValue: unknown }>): void and may be used like this: propertiesChanged({ prop1, prop2 }) {
if (prop1 && prop2) {
// both bindables were changed at the same time (in a batch for example). apply first update strategy
} else if (prop1) {
// only prop1 was changed - apply second update strategy
} else {
// only prop2 was changed - apply third update strategy
}
} So it will not fire for each changed property, but only once for all batch changes. |
馃挰 RFC for synchronous vs asynchronous binding systems
This RFC is an important topic of discussion before we move into release candidate (RC).
馃敠 Context
Currently, the binding system in Aurelia v2 is a synchronous system: changes are immediately notified at the time change happens, rather than queued and notified later, which is the approach in v1.
There are pros and cons to both types, though we will describe the issues from the perspective of a synchronous binding system.
1. the "glitch" problem
A synchronous binding system allows applications to wield great control over all assignments and mutations. Though it sometimes leads to glitches (current industry term) or impossible state.
Consider the following scenario:
when a change happens in
object
, it is propagated toobserver 1
and thenobserver 1.1
before hittingobserver 2
. Ifobserver 1.1
happens to use a value fromobserver 2
, it will be using a stale value of theobject
, sinceobserver 2
hasn't received the new update yet.An example of the above scenario is as follow:
In the above example, we are expecting to ban anyone with the word
Sync
in either their first or last name. But what happens is changingfirstName
toSync
won't result in a[Banned]
tag but a[Badge]
. This is becausetag
property (a computed observer because of@computed
) subscribes tofirstName
first, and it will use the stale value fromfullName
whenfirstName
changes.Here,
firstName
beingSync
butfullName
being an empty string is what we call a "glitch", or an impossible state. This even though can be solved by not allowing cache on the computed value but we can face the same issue without computed values, and having computed values without caching could work for small scale of usage, but it could turn into performance issues.Asynchronous binding systems, depending on their implementations, may or may not have this issue.
If upon the assignment of the value
Sync
to thefirstName
property, an asynchronous binding system only delays the change propagation to the next tick, and then propagates changes in the same way of the synchronous binding system, it will also face with glitches. This is just a "delayed" synchronous change notification.If upon the assignment of the value
Sync
to thefirstName
property, an asynchronous binding system propagates changes of from observer to its subscribers, namelytag
andfullName
properties in our example, but those properties don't immediately recomputed and execute their logic and only start re-computing after all the subscribers have received the notifications to discard their stale computed value, then it's possible that this system will not have issues with "glitches". This mechanism is similar to way the signal proposal works (according to @EisenbergEffect), which is called "push then pull".2. the "state tearing" problem
When there is a group of states that are supposed to be updated together, a synchronous binding system can cause premature change notification, and thus, premature recomputation.
Consider the following example:
When calling the method
nameTag.update('John', 'Doe')
, an error is thrown as the assignment of the valueJohn
to thefirstName
property results in the computed propertyfullName
be run prematurely.Here
firstName
andlastName
even though are visibly updated together at once in the code, don't always trigger changes together at once is what we call "state tearing". In a synchronous binding system, we often seen batch API provided as a way to hold the change propagation until after a certain point. Examples are our ownbatch
or Solid batchAsynchronous binding systems do not have this problem as changes normally are propagated at the next microtask/tick, which means all synchronous code will complete first.
馃搸 An evaluation of other frameworks
Some binding system offers a mixed approach: normal changes are acted synchronously, while computed values will be acted asynchronously. Such is the case with Svelte, as an example. You can test it via the following link: https://learn.svelte.dev/tutorial/reactive-declarations with the following code
You'll see the console logs like this
The fact that
count: 1
being logged before{count: 1}
and{double: 2}
tells us that they are after the promise task, which is asynchronous.Some binding system offers a full asynchronous approach with the ability to have one part stay in synchronous mode. Such is the case with Vue. An example can be seen from its doc https://vuejs.org/guide/essentials/watchers.html#sync-watchers
Some other systems, from other popular frameworks/platforms may offer some different ways of handling these problems, but the above examples (solid, svelte, Vue) seem to be the close and sufficient to describe some differences and options
馃搷 Options
Remaining as a synchronous binding system can be quite compelling from the perspective of the effort required, especially considering we want to go RC within 1 - 2 more beta releases. A synchronous binding system also gives very predictable behavior (words copied from Solidjs doc), which is not always the case with asynchronous binding system, as there will be race condition when app grows without proper state management. Users of Aurelia 1 will be able to recognize this potential issue via the use of
taskQueue.queueMicroTask
when they face timing issues in their Aurelia v1 applications. (Thanks @Vheissu for mentoning this).The text was updated successfully, but these errors were encountered: