-
Notifications
You must be signed in to change notification settings - Fork 27.5k
Binding / Expression Namespaces (better BindOnce alternative) #6354
Comments
I like your solution in part -- specifically, I like the part that it works in expressions, which work in AND out of directives due to $scope.$eval() and similar functions. Also, depending on how this solution and ngBindOnce are implemented in the core, this solution may be easier to implement in some custom directives then ngBindOnce. My use case for ngBindOnce is a search menu with many nested layers. Due to the number of nested repeats it adds easily an estimated 8000+ watchers -- this number is just the search menu and doesn't include the rest of the app. The major advantages that ngBindOnce has to this solution include: (order irrespective)
Though, you mention backwards compatibility in syntax as on of your benefits list so I am willing to stipulate that number 2 may be nullified. personally though, I find the directive name easier to use and learn then the extra namespace syntax. But is just my opinion. All in all, great alternative. As long as the core team adds something to solve this problem, or at least greatly decrease the problem, I will be somewhat more happy. |
@rhettl actually no, your 8000+ watchers would be lumped into 1 watcher and that 1 watcher would trigger (potentially) 8000+ re-evaluations As for the ngBindOnce advantages:
I don't really think adding more directives makes life simpler. It makes it more complicated to refactor, and you lose aspects of fallbacks and back-compat that you would otherwise gain from being able to implement namespaces anywhere an expression could be used. |
My understanding of this is that
As for the "ngBindOnce advantages" response list:
I can see where adding more directives might make re-factoring more difficult. I know that more directives can add more complications, but I also feel that the declarative nature of AngularJS is in the understandable directive. The more distinct a directive name, the more intuitive it is to use and the easier it is to learn for new people. Though I am not sure I see how "[...] you lose aspects of fallbacks and back-compat [...]" Enjoying the discussion, |
No matter what you're going to be storing SOME sort of reference in memory. Keep in mind, if you have 8000+ bind-once directives you are executing the linking function 8000+ times. You may also want to simply look at DOM virtualization for your specific usecase. Regardless, when we're not talking about DOM manipulation, JS can keep up pretty well if it's unavoidable memory storage (it can iterate and modify 400,000 objects in an array in milliseconds, as I've found). Arguing the merits of storing a reference to {{x}} placeholders vs bind-once="x" placeholders vs anything else is kind of moot. In any and all scenarios, SOMETHING is going to be registered in a cached stack. This however is not that big of a deal if done at a bare-minimum use-case. You're also slightly confusing how the watcher works in my proposal. You do NOT have 8000+ functions observing 1 value for changes. You have 8000+ callbacks waiting to be triggered. The namespace watcher, when fired, iterates through these 8000+ callbacks and fires them all. In other words, unless the namespace is triggered, these 'processes/watchers' will never be touched, execute, perform checks, etc. They would simply be inactive functions in memory in a collection. |
As for the merits of back-compat/fallback/api, I personally feel you're better off going into your existing expressions/bindings and adding a prefix than changing which directive you are using altogether. This means that third-party libs, etc, will break if they expect you to be using a specific binding. However, by simply adding additional syntax triggers to expressions, as you stated, you can now use this namespacing anywhere in the codebase. AND third party libs would be more capable of handling namespaces since they can just be stripped out (either by the lib or by angular) vs having to completely switch between which directive you're firing. More directives != simplicity. It means more complexity, more API to learn, and a higher barrier of entry. Part of the beauty of angular as a whole is that you are capable of doing things like $scope.val = whateverTheHellYouWant vs something like new Knockout.Observable('Array') and all that garbage. Keep the API simple and just make it more versatile, rather than creating one directive/method/endpoint/api for every possible variation of a behavior (even if they get broken down internally). |
Okay, thank you for the explanation on multiple accounts,
This meets my #2 above, "[...] register itself with a stack of changes to be made when the personNamespace namespace changes." You sound like you have some experience in the iterative power of JS and I am inclined to trust that. I would still be interested to see the benchmarks after the code has been more or less completed :) To the back-compat paragraph, your argument is reasonable. I have been making a lot of directives lately for work's user interfaces and if one is expecting a particular directive like ngRepeat or ngBind it would be off-putting to suddenly have ngRepeatOnce and ngBindOnce, though I am unable to come with a use case where someone would want to do this w/o allowing the normal use of the original version. I can see how using the expressions would be beneficial, as it would hopefully easily pass through $parse and $eval as current ones do. While writing this I am considering the ramifications of putting one of these namespaced expressions through $parse and how it would work with I agree with much of your last paragraph on "directives != simplicity" but I did not mean to imply the opposite. Anyway, I think this portion is something of opinion on both our accounts as far as which will be more simple. Personally, while I don't see yours as more simple, if it is simple enough and benchmarks better, then I think most of the other issues will resolve them selves eventually. |
Look at text-directives in Angular Light, when you write "{{=model}}" instead of "{{model}}", it works like bindonce. Also you can use "personNamespace": "{{#personNamespace model}}". |
@IgorMinar FYI |
This is a very good proposal. Much much better than the bind-once solution. I hope they'll notice it before they've gotten too far with bind-once. Since the namespace is defined in terms of an expression, that means you'll have to redefine it when an isolated scope-boundary is encountered unless the namespace watcher can somehow be included into the isolated scope. I bring this up because of the case when you need nested components each with their own isolated scope with templates where some bindings should reevaluate when the root level namespace-expression changes. PS: For ng-repeat, I would propose this syntax instead: |
I stayed away from bind-once even thought it was solving one of the biggest painpoints in scaling AngularJS. The proposed solution in this issue looks elegant. +1 for backwards compatibility. Even if the exact solution described in this issue isnt adopted, I like the idea that there arent 2 ways of displaying the data in AngularJS. It should always be expressions, and what we put inside expressions should change the behavior. |
I'm very happy to see cee429f as I am guessing (hoping) that it will eventually lead way to what you guys referred to as Labeled Bindings (my guess would be by doing |
Expressions that start with `::` will be binded once. The rule that binding follows is that the binding will take the first not-undefined value at the end of a $digest cycle. Watchers from $watch, $watchCollection and $watchGroup will automatically stop watching when the expression(s) are bind-once and fulfill. Watchers from text and attributes interpolations will automatically stop watching when the expressions are fulfill. All directives that use $parse for expressions will automatically work with bind-once expressions. E.g. <div ng-bind="::foo"></div> <li ng-repeat="item in ::items">{{::item.name}};</li> Paired with: Caitlin and Igor Design doc: https://docs.google.com/document/d/1fTqaaQYD2QE1rz-OywvRKFSpZirbWUPsnfaZaMq8fWI/edit# Closes #7486 Closes #5408
Anyone know the time frame for this? I also stayed away from bindonce like @jeffjose and instead used a paging solution but this would let you do a lot more with angular. |
It's very unlikely at this point that we'll implement this in Angular 1.x. Between the already implemented one-time binding and major perf The main issue with namespaced binding system is the sheer complexity that Similar to partial digest which we explored quite thoroughly and ultimately
|
Thanks for the info. I didn't really expect this until 2.0 because of how complicated it is. But I just wanted to see what the status this was, and find out if there were any plans to replace bindonce since the repo has been dead for several months. |
I'm generally not convinced that namespaced bindings are the way to go. So And just to be clear when I talk about complexity I don't mean the
|
I'd certainly like to see some kind of solution that optimises evaluation of binding expressions beyond bind-once. Perhaps in the $digest improvements you mentioned or by some syntax that infers idempotency for a given $digest cycle. Certainly two identical binding expressions, especially those that are function calls shouldn't need to be evaluated individually per $digest cycle, once should be enough for all bindings with that same binding text. Presumably it could conceivably go further depending on how the expressions are tokenised. Perhaps caching expression parts, perhaps memoization. Similarly, I wouldn't expect two binding expressions with identical text to create two watchers. Personally, I quite like the namespace approach. A bit like ngInit aliases but more dynamically evaluated, but any other optimisations are good too. |
I think the level of complexity applications (and developers) are getting into in order to circumvent performance issues with AngularJS might merit reconsidering this solution (as over these past several months, I still feel this is a highly viable solution). Just like the 'dot' binding and $scope.$apply confusions, it may simply be a matter of clear instruction and documentation. |
@ProLoser there is a proposal in place at https://docs.google.com/document/d/1XIng9nC_8G48AJ9Pbq8gYSWYvS1dnhiFxpugIKAPBhY/edit |
I don't expect to see it in v1.x but I just was hopeful that it saw it's way into v2.x |
@lgalfaso Actually reading that proposal, that's not what I envisioned namespaces to be. That simply uses a namespace to turn watchers on and off, but that means when they are on, they are extremely inefficient. I was instead proposing that the namespace is a way of lumping watchers together. In other words, namespace bindings are in fact ALWAYS active, however there is only 1 watcher for all of the bindings. |
Would it be at all possible to add this as a note as an alternative proposal? I feel there is too much focus on this 'one-time' binding behavior or turning watchers on and off instead of using namespaces as a way to lump multiple bindings into 1 watcher. In other words, if the namespace watcher <div ng-namespace="person:person.id">
<h1>{{person:person.name}} ({{person:person.age}})</h1>
<h2>{{person:person.title}}</h2>
<p>Gender: {{person:person.gender}}, Company: {{person:person.company}}</p>
</div> |
I think if the reasons that it was felt this api may lead to implementation confusion was because watchers are either listening or not-listening, then perhaps thinking of it in terms of watcher-lumping the api would in fact not be confusing at all. |
This is still in the drawing board, and all feedback is good. If you would like to comment on the proposal, please do so (same goes if you would like to write a new proposal). One thing that is important to keep in mind is that you cannot have two sets of watchers that are both active (but only checked under specific circumstances) as this would imply that there is a need for an inconsistency resolution mechanism (there is one in place now, but it is very simple, adding two disjoin sets of watchers that are active would imply that there is a need for a much more complex algorithm to handle this). Also it is a lot harder to explain. |
I feel like there's still a misunderstanding. I don't think I'm necessarily proposing circumstantial watchers. I'm talking about separating the listeners/watchers (the checks) from the bindings (the evaluations/results/callback/render/output). Essentially, the ng-namespace expression would be added to the watchers list and dealt with normally. All of the namespaced bindings would in fact NOT have watchers whatsoever. Instead, the callback of the namespace watcher would simply trigger the update. It'd be more like having the namespaced bindings listen to a broadcast event that is triggered by the namespace watcher. Like, uh...
And then all expressions that have The only refactor to the existing core would be to have expressions with namespaces subscribe to scope events. |
I'm also unable to comment on that proposal document. Is there seriously an issue with this logic ^? |
I think that what you are saying and what the proposal is, are very similar. The only reason why there is a need for a one-time binding is the async nature of an app. Say you have an expression If we do not use one-time binding, then we would need to add a watcher to If we use one-time binding, then life is easier, as we do not have to keep track of Maybe I am missing something, that would make your solution work better in this scenario. |
I just sort of figured you wouldn't track updates to So we're monitoring
Done. You are deciding between foo and bar? Which one changes more often? Lets say that foo is tied to the page render, but bar is not (tied to something on the page:
My point is I think you're overcomplicating it and ruling out ways of improving performance because we can't cover all 100% use cases, even though we're expanding coverage to 80%. Simply imposing a 1 namespace restriction, and let the dev figure out what namespace (if any) he chooses to use. If all the variables are subject to change, he can simply to forego namespacing. I mean that's all there is to it really. If you want the data to change MORE often than your namespace changes, then DONT namespace. I really don't think it's that complicated. I toyed with the idea of multi-namespace bindings:
Which would then just add a The main use-case for one-time binding is something like static-like rendering. 2 examples are 1: translation and 2: logs. But lets say instead of a timestamp for logs, we use time-ago, so we want to update this value every minute, or every hour, or something. Lets also say we want the user to change the translation on-the-fly. This turns what the developer initially thought would be bind-once into a bind-occasionally scenario. Also, lets say we've got 100s of bind-once bindings on the page, and absolutely none of them are getting values. This would mean that UNTIL they get their initial value, their killing performance and quite possibly reducing the initial page render-time. Namespace binding could very well give BETTER performance in more situations than bind-once. |
Btw, if the expression was an object, you could even create more namespaces as necessary:
Or even The point is NOT to think of namespaces as wether or not watchers are on or off. The point is to think of namespaces as watcher-lumping. We would now have a concrete API for saying that all these expressions on all these different scopes are all actually really just using 1 expression to check for value change. |
I totally get this, and think it would be tremendously valuable. Not least for DRYing out expressions you want to bind in more than one place in the HTML but you don’t want to call a function(). +1 for namespace aliases to watched expressions. From: Dean Sofer [mailto:[email protected]] I just sort of figured you wouldn't track updates to bar. So we're monitoring bar right? Lets say it changes a lot: {{ foo[bar] }} Done. You are deciding between foo and bar? Which one changes more often? Lets say that foo is tied to the page render, but bar is not (tied to something on the page: {{ :bar-ns: foo[bar] }} where as other places you might bind to simply {{ :foo-ns: foo.whatever }} My point is I think you're overcomplicating it and ruling out ways of improving performance because we can't cover all 100% use cases, even though we're expanding coverage to 80%. Simply imposing a 1 namespace restriction, and let the dev figure out what namespace (if any) he chooses to use. If all the variables are subject to change, he can simply to forego namespacing. I mean that's all there is to it really. If you want the data to change MORE often than your namespace changes, then DONT namespace. I really don't think it's that complicated. I toyed with the idea of multi-namespace bindings: {{ :foo-ns bar-ns: foo[bar] }} Which would then just add a $scope.$on to each namespace (I realize this may create redundant processing) but I don't even think THAT level of complexity is really necessary. The main use-case for one-time binding is something like static-like rendering. 2 examples are 1: translation and 2: logs. But lets say instead of a timestamp for logs, we use time-ago, so we want to update this value every minute, or every hour, or something. Lets also say we want the user to change the translation on-the-fly. This turns what the developer initially thought would be bind-once into a bind-occasionally scenario. Also, lets say we've got 100s of bind-once bindings on the page, and absolutely none of them are getting values. This would mean that UNTIL they get their initial value, their killing performance and quite possibly reducing the initial page render-time. Namespace binding could very well give BETTER performance in more situations than bind-once. — |
Hi,
Once a feature is out, it is very hard to make a breaking change to it. This implies that if we know that a specific feature does not cover a reasonable amount of the use cases, then it is better to keep on working than to release something that will break soon. Whenever you state that a simpler solution would cover 80% of the use cases, what scientific method you used to be sure that this simplified version covers 80% of the case? Would you be comfortable if a simplified version is merged and based on community feedback we have to break it in a way that you will need to use a substantial amount of time to accommodate to a new behavior? I am sure that whatever solution we implement, there will be use cases that were not taken into consideration, but I think it is not the same when a solution is build when all the use cases known at the time are covered than having a solution that is known to be a compromise. I see the simplified solution as a compromise that will cause severe headaches to developers. Now, given that this is not in the scope for 1.4, then we have the time to think this thru. Your feedback is greatly appreciated, and it would make the solution better. |
I'd like to pitch in with a solution I cooked up, heavily inspired by a draft (signed(?) by Karl Seamon, Igor Minar, Caitlin Potter) on the subject from August 2014. Scroll down to the very end and you'll end up in the <div binding-notifier="{ u: user }">
{{u: user.name}}
</div>
<div binding-notifier="{ watch: users, notify: 'u' }">
<div ng-repeat="user in u:users">{{u: user.name}}</div>
</div> The 'proposal' outlines some issues on using an event bus for refreshing binds, I'm not so sure I've come around those issues or not (will verify, soon™).
"my solution" to the "problem"
<section bind-notifier="{ u: user, l2: exp2 }">
<header bind-notifier="{ headingLabel: someOtherExpr }">
<h1>{{:headingLabel:h1Expr}}</h1>
<span>{{:u:l2:headingLabel:iUpdateOnAllThree}}</span>
</header>
<span ng-bind=":u:user.name"></span>
<img ng-src=":l2:someImageSrc">
<!-- my favorite... -->
<div bind-notifier="{ ns: data.length }">
<span ng-repeat="d in :ns:data track by $index">{{:ns:d}}</span>
</div>
</section> Where every When a watched expression changes, an event is sent down through the descendant $scope's letting every expression with the An expression can have multiple namespaces attached like so: One could forego the I've tested this with most/if not all the built in Naturally, I would love for this to land in core - but given the rough around the edges state of the current implementation, and the additional complexity that comes with such a system (I personally don't see it), I am prepared for it to remain a third party solution. Is this somewhat close to what you are looking for @ProLoser? I'd love to hear your thoughts on the solution. |
Your "solution" is pretty much what I stated in the very beginning, where as your original example doesn't make a whole lot of sense to me (you're binding an object to the same namespace as a collection, which is wrong). Yes this is close though to what I prescribed. The one note I might add is that multi-namespaced bindings may be adding unnecessary complexity, but I guess they are worth exploring. I think though your example for a multi-namespace binding is a little confusing and possibly unnecessary. Lets say we drop support for multiple namespaces, the api becomes simplified as the user is told to just make a new namespace that encompasses all 3 values: Anyway, TL;DR: You pretty much seem to be implementing things pretty close to how I originally proposed them with some minor tweaks. |
@kasperlewau I've updated my original post at the top to incorporate more recent trends in binding syntax and your object notation for |
The original example was only ever 'stolen' from the August 2014 draft, whereas the second example showcase the current state of things over at camp Kasper. Sorry about any confusion caused there. Don't get me wrong here - I never meant to hijack your proposal with "my solution", I think it's a problem as much as you do that needs solving. I only ever wanted to highlight how it could be done (from an implementation point of view) utilising oneTimeBinds over and over again and hooking into $parse, as opposed to a lot of the current solutions that recompile DOM nodes and/or use custom In regards to multiple namespaces, I quite like the power you get from being able to 'chain' them (I'm currently working on a solution so as to compose them aswell). I'll admit that the example on multi namespaces I've given doesn't make much sense, I'll go get some chow and come back with a more senseful example so as to showcase why I think it should stay. I suppose you could take your example of editA slightly more senseful example of a multi-namespace use case: <div bind-notifier="{ b: bananas, ic: icecream }">
<span>Banana count: {{:b:bananas.length}}</span>
<span>Icecream count: {{:ic:icecream.length}}</span>
<span>Possible Banana Splits: {{:ic:b:icecream.length / bananas.length}}</span>
</div>
It's a very naive example but I think it does show merit for keeping it around. Assuming we don't have a significant amount of fluctuation in the amount of bananas our ice cream we have - in which case a regular binding would be the way to go. |
@ProLoser I've updated my reply with a multiple namespaces example that I feel is a little easier to reason about. |
@kasperlewau Sorry for the tardy response, I was responding weird. Your solution is awesome! |
We are trying to add #10658 as a general way to detach / re-attach scope listeners on demand. Bind / Expression namespaces as seen in this PR will not be implemented. |
If you want to help out with #10658, please ping us in the PR. |
So lots of people are asking for 'BindOnce' but I actually don't believe this is the best approach to handle performance. Not only does it require specific directives, it also doesn't properly tackle the 'bind-occasionally' scenario, or when it does, it requires a completely different directive. Instead I propose:
Expression Namespaces
Examples:
ng-bind="{ personNamespace: person.name }"
{{personNamespace:: person.age}}
ng-if="personNamespace:: person.alive"
ng-repeat="personNamespace:: child in person.children "
$scope.$watch('personNamespace:: person.height', ... )
{{i18n:: i18n['welcome.message']}}
ng-if="login:: hasPermission('DeleteUsers')"
These would all be handled by:
ng-namespace="{ personNamespace: person }"
or$scope.$namespace({ personNamespace: 'person' })
Only the
ng-namespace
expression is evaluated on every $digest. When the expression changes in value, all namespaced bindings get re-evaluated.The
person
value would be watched normally, and every time it changes, thepersonNamespace
bindings throughout the rest of the application would be refreshed.This is pretty much JUST 'bind-occasionally' since if the 'occasional' occurance is only once, then you are getting the same benefit of a bind-once.
Benefits:
{{once:: person.name}}
{{always:: person.selected}}
Issues:
ng-namespace
(a directive) may be confusing alongside namespaces in watchers (controllers). I am not 100% sure the watcher version is necessary however.Implementation
I was discussing how the best way to write an efficient translation solution and we realized that instead of using watchers, it's better to use
$broadcast()
to signify that a namespace digest has been triggered. I'm not necessarily saying this is how it should be implemented in the core, but this was one idea we came up with.This separates the expression being WATCHED from the expression being ECHO'd
It's like saying "Although I want to spit out
person.age
I want you to just monitorperson
for changes". However what is being 'monitored' is defined in yourng-namespace
. Each binding or expression is no longer being watched. Instead they are waiting for some sort of$broadcast('namespace-person')
instead, and only 1 watcher is ever actually used.TL;DR:
Using expression 'namespaces' we no longer deal with turning off / on bindings. Instead, only 1 expression is 'watched' and all the other bindings are tied to this one expression. There is no toggling on and off of bindings.
The text was updated successfully, but these errors were encountered: