Angular中的变更检测
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Angular中的变更检测相关的知识,希望对你有一定的参考价值。
参考技术A 变更检测就是Angular检测视图与数据模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,就把数据同步到视图上。我们先看下面这个例子
通过以上例子我们可以总结出来,在异步事件发生的时候可能会使数据模型发生变化。可是angular是如何检测到异步事件发生了呢?这还要说起zone.js。
官方定义zone.js是javascript的线程本地存储技术,猛地一听感觉好高大上,其实zone.js就是一种用来拦截和跟踪异步工作,为JavaScript提供执行上下文的插件。
那么它是如何感知到异步事件呢,其实方法相当简单粗暴,zone.js采用一种叫做猴子补丁 (Monkey-patched)的方式,将JavaScript中的异步任务都进行了包装,这使得这些异步任务都能运行在Zone(一个全局的对象,用来配置有关如何拦截和跟踪异步回调的规则)的执行上下文中,每个异步任务在 Zone 中都是一个任务(Task),除了提供了一些供开发者使用的钩子外,默认情况下Zone重写了以下方法:
zone.js部分源码
通过打印window对象我们可以发现zone.js对异步方法进行了封装,非异步方法并没有处理。
zone.js本身比较庞大复杂,这里不做深入研究,对它的原理感兴趣的可以看一下这篇文章-zone.js。我们这里主要是了解它是怎么配合Angular工作的即可。
在 Angular 源码中,有一个 ApplicationRef 类,其作用是当异步事件结束的时候由 onMicrotaskEmpty执行一个 tick 方法 提示 Angular 执行变更检测及更新视图。
调用tick方法。其中this._zone 是NgZone 的一个实例, NgZone 是对zone.js的一个简单封装。
tick函数对所有附在 ApplicationRef上的视图进行脏检查。
Ok,我们现在已经知道Angular怎么监听异步事件了,那么当监测到异步事件后是怎么判断是否需要更新视图呢?其实比较简单,Angular通过脏检查来判断是否需要更新视图。脏检查其实就是存储所有变量的值,每当可能有变量发生变化需要检查时,就将所有变量的旧值跟新值进行比较,不相等就说明检测到变化,需要更新对应视图。当然,实际情况肯定不是这么简单,Angular会通过自己的算法来对数据进行检查,对算法感兴趣的可以参考这篇文章-Angular的脏检查算法。
Angular 应用是一个响应系统,首次检测时会检查所有的组件,其他的变化检测则总是从根组件到子组件这样一个从上到下的顺序开始执行,它是一棵线性的有向树,任何数据都是从顶部往底部流动,即单向数据流。怎么证明呢?看这个例子
运行以后我们会得到如下结果,可以看到首次检测时检查了所有组件,包括ReferComponent,检测从上到下逐个检测。点击改名按钮后再次检测时则只检测有变化的那一侧组件(RankParentComponent,RankChildrenComponent)。其中我们可以观察到,虽然在AppComponent中输入属性也发生了变化并且也更新了视图,但是ngOnChanges钩子却没有检测到变化,注意这是一个坑。
那么什么是单向数据流呢?其实简单理解就是angular检测到数据变化到更新完视图的过程中数据是不应该被改变的,如果我们在这期间更改了数据,Angular便会抛出一个错误,举个例子,我们在RankChildrenComponent的ngAfterViewChecked钩子函数中更改childName的值,在控制台会看到如下错误。
如果必须要更改这个属性的值,能不能做呢?答案是可以的。结合刚次提到的单向数据流,如果我们把这次数据变更放到下一轮Angular变更检测中,就能解决这个问题了。怎么做呢?刻意异步一下就行了。是不是很神奇?
至于angular为什么要采用单向数据流,其实也很好理解,最主要的就是防止数据模型和视图不统一,同时也可以提高渲染的性能。
讲了这么多,所以到底有什么用呢?其实在 Angular 中,每一个组件都都它自己的检测器(detector),用于负责检查其自身模板上绑定的变量。所以每一个组件都可以独立地决定是否进行脏检查。默认情况下,变化检测系统将会走遍整棵树(defalut策略),但我们可以使用OnPush变化检测策略,利用 ChangeDetectorRef实例提供的方法,来实现局部的变化检测,最终提高系统的整体性能。
来,举个例子。在ReferComponent中,我们设个定时器2秒以后更新一个非输入属性的值,在默认策略时,可以发现2秒以后视图中的值发生了改变,但是当我们把策略改为Onpush时,除了在AppComponent点击按钮改变输入属性justRefer外,其他属性改变不会引起视图更新,ReferComponent组件的检测也被略过。我们可以这么总结:OnPush 策略下,若输入属性没有发生变化,组件的变化检测将会被跳过。
可是我就是要更改非输入属性怎么办呢?别急,Angular早就为你想好了。在Angular中,有这么一个class:ChangeDetectorRef ,它是组件的变化检测器的引用,我们可以在组件中的通过依赖注入的方式来获取该对象,来手动控制组件的变化检测行为。它提供了以下方法供我们调用
现在我们来试试解决刚才那个问题,我们对ReferComponent做如下改造。
ok,现在看到在Onpush策略下手动修改非输入属性的值,视图也可以及时更新了。其他的几个方法也都大同小异,感兴趣的可以逐个试试。
如何在 Angular2 中触发变更检测? [复制]
【中文标题】如何在 Angular2 中触发变更检测? [复制]【英文标题】:How to trigger change detection in Angular2? [duplicate] 【发布时间】:2016-06-01 21:28:20 【问题描述】:我正在创建一个调用 Facebook javascript api 的 Facebook 服务,并且想知道如何在我的值更新时最好地实现更改检测。
我有一个 UserService
,它有一个 currentUser
属性,它是一个 BehaviorSubject:
currentUser: Subject<User> = new BehaviorSubject<User>(new User(null));
当我想更新用户以响应 facebook javascript sdk 告诉我用户已登录或注销时,我会更新该用户并需要在 ApplicationRef
上调用 tick()
:
updateUser(user: User)
console.log('UserService.updateUser:', user);
this.currentUser.next(user);
this.appRef.tick(); // UI does not update without this
constructor(facebook: Facebook)
this.facebook.facebookEvents.filter(x => x != null
&& x.eventName == 'auth.authResponseChange')
.subscribe((event) =>
this.updateUser(new User(event.data));
在我的组件中,我将来自用户服务的“currentUser”存储在构造函数中并绑定到 value 属性:
<h2>Logged into Facebook as currentUser.value.name</h2>
<p>Is this you? <img src="currentUser.value.profilePicUrl"></p>
我做错了吗?有没有比在外部库触发更改后必须调用 ApplicationRef.tick() 更好的方法?
编辑
我尝试使用 NgZone,但它不起作用,使用不同的事件将提要中的帖子作为服务页面返回:
constructor(userService: UserService, private ref: ApplicationRef, private zone: NgZone)
...
this.postsSubject.subscribe((post) =>
this.zone.runOutsideAngular(() => // doesn't do anything
this.posts.push(post);
console.log('postsSubject POST, count is ', this.posts.length);
ref.tick(); // required to update bindings
);
控制台显示计数递增,但仅当我添加 ref.tick()
调用时,html 绑定 posts.length
才会更新...
我想我在某处看到您可以从***应用程序组件中为任何组件提供“输入”,这可能是登录用户的方式,但不是其他调用,例如在提要中获取帖子。 ..
【问题讨论】:
我不知道,但如果这对你有帮助,那就好了,***.com/questions/35500890/… currentUser 是一个 BehaviorSubject。您必须使用异步管道 (currentUser | async)?.value?.name
订阅它。
【参考方案1】:
我做错了吗?有没有比在外部库触发更改后必须调用
ApplicationRef.tick()
更好的方法?
这取决于。如果您在 Angular 之外调用 Facebook API(即在 Angular 之外注册异步事件处理程序),那么您需要手动运行更改检测作为处理任何事件的一部分。你可以
注入NgZone
,然后在该对象上调用run()
,这将在Angular区域内执行传递(回调)函数,然后自动调用ApplicationRef().tick()
,这将对整个应用程序运行更改检测,或者
自己注入ApplicationRef
并调用tick()
,或者
自己注入 ChangeDetectorRef
并调用 detectChanges()
-- 这只会在一个组件及其子组件上运行更改检测
注意,如果你注入NgZone
,你就不想使用runOutsideAngular()
。这将在 Angular 区域之外执行传递的(回调)函数,并且不会调用 ApplicationRef().tick()
,因此不会执行更改检测。
如果您在 Angular 中调用 Facebook API,您可能能够避免上述所有情况,因为 Angular(通过使用 Zone.js)应该对异步事件调用进行猴子补丁。然后,当您的事件触发时,ApplicationRef.tick()
将自动被调用。有关此方法的更多讨论,请参阅https://***.com/a/34593821/215945。
【讨论】:
因此,避免整个应用程序更改检测的最佳方法可能是对组件使用 ChangeDetectorRef 并订阅用户更改事件并从那里调用它? @JasonGoemaat,如果只有一个组件的视图需要更新,那么这可能是最有效的方法。 (请注意,使用detectChanges()
检测到组件及其子项已更改。我将更新我的答案。)【参考方案2】:
我不确定它是否更好。我还能想到其他几种方法。
使用 NgZone
您可以注入 NgZone 并使用回调执行 run 方法:
NgZone.run(()=>this.currentUser.next(user));
使用 setTimeout
setTimeout 会自动触发变化检测:
setTimeout(()=>this.currentUser.next(user));
【讨论】:
正确的是 ngZone,我们在 GoogleMaps 在地图上放置图钉时遇到了类似的问题。 你不能使用NgZone.run
,必须注入它的实例。
而且使用setTimeout是不对的,它只会触发NgZone实例的outterZone(aka Zone.current)。所以它不会触发角度的变化检测。
@e-cloud,你确定吗? setTimeout 肯定会在 Angular 区域 (forkInnerZoneWithAngularBehavior) 内运行,并且在离开时会耗尽微/宏任务并触发更改检测。【参考方案3】:
您能否告诉我们您初始化Facebook
提供程序以及您在组件中的服务的方式?
选项#1
你的行为让我改变了 Facebook 代码在 Angular 区域之外运行,因此当事件触发时,Angular 更改检测不会运行。你
您也可以手动运行更改检测:
import Component, ChangeDetectorRef from 'angular2/core';
@Component(
(...)
)
export class MyComponent
constructor(private cdr:ChangeDetectorRef)
someMethod()
this.cdr.detectChanges();
查看这些问题:
"async" pipe not rendering the stream updates How to force a component's re-rendering in Angular 2?选项 #2
也许您的事件在组件注册currentUser
主题的回调之前被触发。在这种情况下,您可以稍后触发事件...
【讨论】:
以上是关于Angular中的变更检测的主要内容,如果未能解决你的问题,请参考以下文章
我在 Angular 变更检测中的断点未在 checkAndUpdateView() 上触发