-
Notifications
You must be signed in to change notification settings - Fork 27.5k
feat($rootScope): allow suspending and resuming watchers on scope #10658
Conversation
This can be very helpful for external modules that help making the digest loop faster by ignoring some of the watchers under some circumstance. example: https://github.com/shahata/angular-viewport-watch
Any thoughts about this? I had to do really hacky tricks in order to do this without changing angular... |
/cc @petebacondarwin |
This issue showed up several times in different forms and shape. Many of us |
I think we should actually use the I'm going to put up more/better template docs this week, and present the idea in the team meeting on monday, so I think we're going to hold off on sending out process emails until there's a bit more feedback from the team. But you can share your doc internally before that. |
+1 |
I wonder if this falls foul, in a similar way to #10221? |
+1 on this pull request I too ended up with the same strategy to pause/suspend watchers (skipping a flagged scope during the digest loop) when we talked about this in #5301 |
+1 on this. @petebacondarwin, @shahata, while there is a significant amount of opportunity here to shoot yourself in the foot, I believe this provides what is an essential feature as applications get more mature in angular. Here is a sample of the hundreds of articles about performance and tools I keep finding referenced to support making angular a viable system to use in larger applications.
The common thread I find in every performance article is that we need to reduce watches in order to ensure performance. The new bind once syntax is based entirely on that goal, and the overuse of (https://github.com/Pasvaz/bindonce) by people advocating speed in angular. It gives you many varied and awful patterns for shooting yourself in the foot. This PR is a much more subtle methodology for achieving the same intent as Lets say a developer wants to build a general purpose tab directive. As is users of that directive can easily shoot themselves in the foot by not properly using bind once and limiting total watches. With this capability a plugin developer can disable the scope of non rendered tabs. In a slightly more complex example. Development of data grids. Currently, in order to really display a large grid of data you absolutely must do virtual rendering, as is done in UI-Grid. For most data sets in modern browsers we could progressively render and leave the static html on the page with little to no performance drop. However, in angular we can't really achieve static output without making it truly static. Imagine if we made a simple directive that progressively rendered the content down the page, then disabled the scope processing for all offscreen components. Digesting components as they scroll into view becomes trivial if all we need to do is update the on screen components to be enabled, and trigger a digest. As an offshoot we don't have to do hacks like https://github.com/shahata/angular-viewport-watch, or https://github.com/scalyr/angular, in order to remove watchers. TLDR; Those of us who know how to not shoot ourselves in the foot with this can use it to write libraries that help others not shoot themselves in the foot in other ways. |
Hi, @park9140 brings some interesting points, so I would like to write some of the issues that this PR brings. TL;DR Consistency modelRight now, angular consistency model is very simple. Eg, the two-way binding in AsyncIf a directive performs anything asynchronous that needs to be reflected, then this directive cannot be inside a scope that gets suspended. The main issue is that in most cases a directive should not care what other directives are inside of it, they should only care of the behavior they bring to the table. TranscludeIf a directive uses a transclusion, then it should never suspend a scope (unless it has complete control of the content being transcluded). The reason being that even when the scope look like a tree, the inherence model does not follow the same tree and a suspended scope will also suspend parts of something that looks like an outer scope, but in reality is a child scope. Extending another directiveA directive that uses suspend cannot be safely extended, this is, without understanding all the ins and outs of when this happens. In most cases, this implies that whoever does this will need to read the code of the other directive. Creating a directive on an html standard elementGiven that a directive does not have control of what directives will be executed inside it (as a developer may decide to create a directive on a standard element), then a directive is never sure that by suspending a scope it is breaking some other directive. In most cases, this implies that you will need to control 100% of all the directives in an app and reusability can be problematic. Ownership of $scopeRight now, there is no clear ownership model between directives and scope. Some people may think that a directive sits on top of a scope (this is that the scope owns the directive) or that they are independent. This change makes a bold claim, a directive that suspends a scope, it owns the scope (as it changes it behavior and everything in it). There are other minor nits in the PR that are not clear how things are affected. Eg, if a scope is suspended twice, should it need to be resumed twice for it to be active? This is important when there are two directives in the same scope use this trick. Each of the options opens another source of conflicts. If a feature is made part of the core, we are saying that this feature is good for broad use, not made-to-order for a handful of directives. If we claim that the way to make your application more performant is this, then it needs to support the use cases that developers are used to. I am not sure that this feature can make these claims. |
Consistency Model This is not as simplistic as you have pointed out, enormous numbers of directives bypass this by entirely bypassing scope as soon as they require any level of 'bare metal' performance. Anything that executes outside the digest cycle currently has very limited options for restoring consistency after the completion of any given bare metal action. Many take the easy route out and trigger something that calls $rootScope.$apply() in order to attempt to ensure consistency. Core angular code does this regularly. 90% of the time I see a digest cycle called it is absolutely unnecessary. Async I do not understand why async operations done by a directive can't be done when the directive is suspended. I suppose you might have a concurrency issue where an async operation could have the wrong data on the scope at the time of processing on a suspended scope, but you shouldn't be accessing scope from a non digest locked operation anyway, and if you are operating on a scope that is suspended the scope interacting portion of your async operation will not run until the scope is resumed. Transclude Im starting to think that a directive is necessarily the proper place to be doing this kind of control. It seems like something that should be reserved for use by your application controllers. Pattern wise it really isn't up to a directive to do this kind of application control. In every situation where I have had the need for this it really should be handled by a controller. Ownership of scope I'd argue that it is fairly clear that scope is a shared resource, except under certain circumstances. Isolate scopes are definitely owned by the directive that makes them. When there is a controller for a section the scope should be under that controller's scope of control. I think I can agree that there are issues with this methodology being a public api and open to everyone to just extend. However, the core capability absolutely must exist for angular to really be able to handle larger applications. Just today I implemented this in my application. The application needs to be able to quickly switch up a multiple annotations on a large grid of data. Using it in my controllers only, I was quickly able to decrease the points of UI thread lockout caused by digesting a much to large active digest tree. I rewrote ng-show and ng-hide to make large numbers of hidden annotations not get digested, but still allowed the core html to be rendered in the initial render state so that displaying annotations could be done very efficiently even with the enormous dataset. TLDR; In the large majority of cases a modern browser can handle way more rendered dom than can be actively controlled by angular in a single cohesive scope tree. Isolating application layers such that global digests are controlled to only work on active code is essential. |
@park9140 I think we mostly in sync, in fact many of the issues that you are pointing out (and the possible solutions) are implemented in Angular 2. Now, for 1.x, many of these changes would imply breaking changes that would be hard for developers to adapt to and need to be documented, and this PR documentation changes are super minimal.
If there is a sub-directive that makes a change that needs to be reflected on the DOM, then it will not be reflected. This implies that this sub-directive will need to contact all parent controllers that suspended a scope and ask them to resume it once so this change it reflected. This can make the solution worst than what we have now.
A directive cannot extend another directive (and this is something developers do a lot)
Agree 100%, something must be made. In fact, this is not the first attempt at solving this issue, but whatever solution ends up in the core needs to be flexible enough to be useful for the majority of users while at the same time do not create any new sharp edges that people have to be aware of. |
If I can - I'm using one time bindings. Now to reset those bindings (to calculate them once again) I'm using How about giving new angular build-in directive Good idea? |
It ends up that landing this would change some constraints in a way that makes the migration to Angular 2 harder. It is a -1 from me |
@lgalfaso - can you elaborate? |
@petebacondarwin will start with isolated directives as it is simpler. In Angular 1, the two statements below are either both true or both false
If both of these statement are true, then the directive is active (invented word just to explain this easier). If both are false, then the directive is inactive (and if things are right, the GC should take care of it including $element). With this change, this is no longer true, as it is possible for watchers in the scope not to get called and $element to be attached to $document (and the directive should remain active). This is, by landing the PR we are breaking something that devs might rely on (that by itself is not necessarily bad if the end result is something better), but it also does so in a way that is less aligned to Angular 2. |
nice, this feature is already in Angular 2/4+ as ChangeDetectorRef.detach() ChangeDetectorRef.reattach() ChangeDetectorRef.detectChanges() |
@gdi2290 - yes, but it has slightly more reliable behaviour in Angular; in AngularJS there are going to be a load of corner cases that could bite us. |
Closing in favour of #16308 |
Has anyone here used Eg @park9140, you said you "rewrote ng-show and ng-hide". Was it using a modified AngularJS like in this PR? |
This can be very helpful for external modules that help making the digest loop faster by ignoring some of the watchers under some circumstance. example: https://github.com/shahata/angular-viewport-watch