[译] $digest 在 Angular 中重生
原文链接: Angular.js’ $digest is reborn in the newer version of Angular
我使用 Angular.js 框架好些年了,尽管它饱受批评,但我依然觉得它是个不可思议的框架。我是从这本书 Building your own Angular.js 开始学习的,并且读了框架的大量源码,所以我觉得自己对 Angular.js 内部机制比较了解,并且对创建这个框架的架构思想也比较熟悉。最近我在试图掌握新版 Angular 框架内部架构思想,并与旧版 Angular.js 内部架构思想进行比较。我发现并不是像网上说的那样,恰恰相反,Angular 大量借鉴了 Angular.js 的设计思想。 其中之一就是名声糟糕的 digest loop: 这个设计的主要问题就是成本太高。改变程序中的任何事物,需要执行成百上千个函数去查询哪个数据发生变化。而这是 Angular 的基础部分,但是它会把查询限定在部分 UI 上,从而提高性能。 如果能更好理解 Angular 是如何实现 digest 的,就可能把你的程序设计的更高效,比如,使用 所以大量有关 Angular 的文章教程里都宣称框架里不会再有 digest 的必要性开始前让我们先回忆下 Angular.js 中为何存在 有两种方式来检测变化:需要使用者通知框架;通过比较来自动检测变化。 假设我们有如下一个对象: let person = {name: 'Angular'}; 然后我们去更新 constructor() { let person = {name: 'Angular'}; this.state = person; } ... // explicitly notifying React about the changes // and specifying what is about to change this.setState({name: 'Changed'}); 或者强迫用户去封装该属性,从而框架能添加 let app = new Vue({ data: { name: 'Hello Vue!' } }); // the setter is triggered so Vue knows what changed app.name = 'Changed'; 另一种方式是保存 if (previousValue !== person.name) // change detected,update DOM 但是什么时候结束比较呢?我们应该在每一次异步代码运行时都去检查,由于这部分运行的代码是作为异步事件去处理,即所谓的 Virtual Machine(VM) turn/tick(注:Virtual Machine 的理解可参考 VM),所以可以紧接着在 VM turn 的后面,执行数据变化检查代码。这也是为何 Angular.js 使用 change detection mechanism that walks the tree of components,checks each component for changesandupdates DOM when a component property ischanged。 如果我们这么去定义 Angular.jsAngular.js 使用
$watch(watcher,listener); 所以,如果我们有一个带有 $watch(() => { return person.name },(value) => { span.textContent = value }); 这与插值和 <span [textContent]="person.name"></span> 由于存在很多组件,并组成了组件树,每一个组件都有着不同的数据模型,所以就存在分层的 现在,在
AngularAngular 并没有类似 Angular.js 中 它们也很特别:只追踪模型变化,而不是像 Angular.js 追踪一切数据变化。每一个组件都有一个 比如, <h1>Hello {{model.name}}</h1> Angular Compiler 会生成如下类似代码: function View_AppComponent_0(l) { // jit_viewDef2 is `viewDef` constructor return jit_viewDef2(0,// array of nodes generated from the template // first node for `h1` element // second node is textNode for `Hello {{model.name}}` [ jit_elementDef3(...),jit_textDef4(...) ],... // updateRenderer function similar to a watcher function (ck,v) { var co = v.component; // gets current value for the component `name` property var currVal_0 = co.model.name; // calls CheckAndUpdateNode function passing // currentView and node index (1) which uses // interpolated `currVal_0` value ck(v,1,currVal_0); }); } 注:使用 Angular-CLI 所以,即使 In development mode,tick()also performs a secondchange detection cycleto ensure that no further changes are detected. 上文说到在 就像 Angular.js 一样,在 Angular 中变更检测也同样是由异步事件触发(注:如异步请求数据返回事件;用户点击按钮事件; Angular 强调所谓的单向数据流,从顶部流向底部。在父组件完成变更检测后,低层级里的组件,即子组件,不容许改变父组件的属性。但如果一个组件在 DoCheck 生命周期钩子里改变父组件属性,却是可以的,因为这个钩子函数是在更新父组件属性变化之前调用的(注:即第 6 步 DoCheck, 在 第 9 步 updates DOM interpolationsfor thecurrent viewif properties oncurrent viewcomponent instance changed 之前调用)。但是,如果改变父组件属性是在其他阶段,比如 AfterViewChecked 钩子函数阶段,在父组件已经完成变更检测后,再去调用这个钩子函数,在开发者模式下框架会抛出错误: Expression has changed after it was checked 关于这个错误,你可以读这篇文章 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error 。(注:这篇文章已翻译) 在生产环境下 Angular 不会抛出错误,但是也不会检查数据变化直到下一次变更检测循环。(注:因为开发者模式下 Angular 会执行两次变更检测循环,第二次检查会发现父组件属性被改变就会抛出错误,而生产环境下只执行一次。) 使用生命周期钩子来追踪数据变化在 Angular.js 里,每一个组件定义了一堆
在 Angular 里却是这么实现这些功能的:可以使用 OnChanges 生命周期钩子函数来监听父组件属性;可以使用 DoCheck 生命周期钩子来监听当前组件属性,因为这个钩子函数会在 Angular 处理当前组件属性变化前去调用,所以可以在这个函数里做任何需要的事情,来获取即将在 UI 中显示的改变值;也可以使用 OnInit 钩子函数来监听第三方组件并手动运行变更检测循环。 比如,我们有一个显示当前时间的组件,时间是由 function link(scope,element) { scope.$watch(() => { return Time.getCurrentTime(); },(value) => { $scope.time = value; }) } 而在 Angular 中是这么实现的: class TimeComponent { ngDoCheck() { this.time = Time.getCurrentTime(); } } 另一个例子是如果我们有一个没集成在 Angular 系统内的第三方 function link(scope,element) { slider.on('changed',(slide) => { scope.slide = slide; // detect changes on the current component $scope.$digest(); // or run change detection for the all app $rootScope.$digest(); }) } Angular 里也同样原理(注:也同样需要手动触发变更检测循环, class SliderComponent { ngOnInit() { slider.on('changed',(slide) => { this.slide = slide // detect changes on the current component // this.cd is an injected ChangeDetector instance this.cd.detectChanges(); // or run change detection for the all app // this.appRef is an ApplicationRef instance this.appRef.tick(); }) } } (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |