$digest 在Angular中的新生

news/2024/11/8 14:11:48
原文地址: Angular’s $digest is reborn in the newer version of Angular

我已经从事Angular.js相关方面的工作好几年了,尽管这是一个饱受批评的框架,但我依旧认为它是极其出色的。一开始从《构建你自己的Angular.js》这本书入门,几年间我也阅读过框架的大部分源码,因此我坚信对于Angular.js内部的工作原理我有扎实的基础,也很好的领会了框架的思想。现在,对于新版Angular我试图达到与Angular.js相同的理解水平并对比版本间实现思想的差异。我发现,与网上声称的相反,Angular从前任借鉴的很多思想。

一个臭名昭著的思想是digest循环:

这会造成可怕的性能开销。应用中的任何改变都会导致成百上千的函数寻求变化。这是Angular(Angular.js)的基础组成部分,为了保持高性能,这将限制住构建应用时的UI数量。

尽管对angular(Angular.js)中的digest的实现有比较好的理解,但设计出高性能的应用依旧存在可能。比如说,选择性的使用$scope.$digest取代$scope.$apply以及拥抱不可变对象(译者注1)。事实上,理解框架内部实现才是构建高性能应用的必经之路,但这确实是大部分人的拦路虎。

这也难怪大部分教程都声称Angular摒弃了$digest循环。这个观点很大程度上取决于你对digest的定义,但我认为,鉴于其目的,这是一个误导性的说法。digest依旧存在,是的,我们不再明确的使用scopes和watchers,也不再调用$scope.$digest,但是遍历组件树、隐式调用watcher以及对DOM的更新,这些变化检测的机制给了Angular第二次生命。完全的重写,更强大的性能。

本文探讨了digest在Angular.js和Angular中实现的不同,无论对Angular的开发者还是那些想把项目从Anguar.js迁移到Angular上的人来说都是很有帮助的。

digest的必要性

在开始之前,让我们先回忆一下为什么digest会首先出现在angular.js中,很多框架都解决了数据模型(JavaScript对象)和UI(浏览器DOM)之间的同步问题,其中最大的挑战是对数据变化知悉的实现。我们把验证变化的过程叫做变化检测,它的实现是现今主流框架的最大区别,我计划写一篇关于变化检测在各个已存在框架中实现的对比,如果你对此感兴趣,请关注我。

有两种变化检测的方法 — 用户通知框架或者通过比较自动检测变化。假设我们有如下对象:

let person = {name: 'Angular'};

并且已经更新了name属性,那么框架是如何知道它已经被更新了呢?一种方法是要求用户去通知框架:

constructor() {
    let person = {name: 'Angular'};
    this.state = person;
}
...
//明确的对变化做出通知
this.setState({name: 'Changed'});

或者强迫对属性使用包装器,这样框架就可以对其添加setter:

let app = new Vue({
    data: {
        name: 'Hello Vue!'
    }
});
// 这个setter被触发,这样Vue就知道什么改变了
app.name = 'Changed';

另一种方法是将当前值与前值对比:

if (previousValue !== person.name) // 变化检测,更新DOM

每次运行代码都会伴随着验证的执行,那么什么时候应该完成比较呢?我们知道异步事件触发了上述代码的运行—称之为虚拟机轮询(VM turn/tick),我们可以在每次循环结束后开始校验,Angular.js中的digest也是这么做的,说到这里,我们可以给digest下个定义:

是一种变化检测的机制,通过遍历组件树,校验每个组件的的变化,并且在组件属性变化时更新DOM。

如果我们对digest如此定义,我敢断定在新版的Angular中这种主要机制没有改变,改变的仅仅是digest的具体实现。

Angular.js

Angular.js中使用了观察者(watcher)和监听器(listener)的概念。观察者函数会返回一个被观察的值,通常情况下这会是数据模型的属性,但这是不一定的-我们可以跟踪作用域上组件的状态、计算值、第三方组件等等。如果(watcher)返回的值与前值不同,那么angular就会调用监听器,监听器通常用于更新UI。

这些都反应在$watch这个函数参数中:

$watch(watcher, listener);

因此,如果我们在html(如:<span>{{name}}</span>)中使用了person对象的name属性,那么我们可以按照如下来代码追踪属性、更新DOM:

$watch(() => {
    return person.name
}, (value) => {
    span.textContent = value
});

这本质上就是angular.js中的插值表达式和指令(如:ng-bind)的实现。angular.js利用指令把数据映射到DOM中。新版Angular已经不这么做了, 它使用属性映射来连接数据模型和DOM。前面的例子现在是这样实现的:

<span [textContent]="person.name"></span>

因为我们有很多组件,每个组件拥有不一样的数据模型,因此我们拥有一个与组件树非常类似的watcher的层级结构。顺便说一下,watcher是使用$scope分组访问的。

Angular.js中digest 遍历watcher树并更新DOM,通常情况下,如果你使用现有的机制如$timeout,$http,$scope.$apply,$scope.$digest,那么每一次的异步事件会都会触发digest 循环。

观察者(watchers)按照严格的顺序触发—先是父级组件随后才是子组件。这有一定的道理,但某些情况下也会造成不好的影响。一个watcher的监听器(listener)存在各种各样的副作用,其中就包括更新父级组件的属性。如果父组件的监听器已经执行,但是子组件又更新了它的属性,那么这个变化将不被检测到。这就是为什么,digest 循环不得不运行多次才能稳定(确保没有更多变化)。循环次数被限制在10次。这个设计是有缺陷的,Angular已不再采用。

Angular

Angular 没有类似于Angular.js中的watcher的概念,但是模型属性的追踪还是存在的。这些更新的方法在框架编译时产生并且无法访问。它们也与底层的DOM有着强连接。这些方法被存在View的一个属性名叫updateRender的方法中。

这些方法是非常明确的,它们只追踪模型的变化而不是像Angular.js那样追踪所有。每一个组件有且仅有一个观察者(watcher),用于跟踪在模板中使用的所有组件属性。Angualr使用checkAndUpdateTextInline这个方法来追踪属性而不是返回一个值。这个方法对比当前值和前值而后更新DOM。

举个例子,AppComponent中存在如下模板:

<h1>Hello {{model.name}}</h1>

这将被编译成如下代码:

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, 0, currVal_0);
        });
}

因此,即使watcher的实现方式不同,但是digest 循环依旧存在。只是换了个名称而已。

在开发者模式中,tick()也会执行第二次以确保没有检测到其他改变。

我前面提到在angular.js中,digest是通过遍历watcher树并更新DOM的。在Anuglar中同样的事情也在发生。Angular通过遍历组件树并调用渲染更新函数来实现变化检测。这作为检测和更新视图过程的一部分,我已经在“你所要知道的所有关于Angular变化检测”中说的很详细。

正如Angular.js,在新版Angular中变化检测也是由异步事件触发。不同的是Angular使用zone接管了几乎所有异步事件,对于大部分异步事件而言无需手动触发变化检测。zone订阅了onMicrotaskEmpty事件,在每个异步事件完成后将获得通知。如果在当前VM轮询中没有microtasks需要执行(译者注2),那么这个事件就会被触发。当然,变化检测也可以通过view.detectChanges或者ApplicationRef.tick手动被执行。

Angular强迫使用自上而下的单项数据流(译者注3)。如果父组件的更改处理完毕了,那么子组件更新父组件的属性是不被允许的。 如果你在组件的DoCheck钩子中执行父组件属性的更新,这是可以的,因为这个生命周期的钩子在属性变化检测之前调用。但是这个操作在其他步骤执行,比如说,在AfterViewChecked钩子中,在开发者模式下就会有如下错误:Expression has changed after it was checked。想了解关于此类错误的更多信息,你可以阅读:你所需要知道的 ExpressionChangedAfterItHasBeenCheckedError错误

在生产环境中这不会报错,但Anuglar不会检测这些变化直到下一轮脏值检测。

使用生命周期的钩子来追踪变化

在angular.js中每个组件都定义了一系列的watchers来追踪以下内容:

* 父级组件的绑定

* 自身组件属性

* 计算值

* 第三方插件

以下是这些功能在Angular中实现。为了跟踪父组件属性,我们现在可以使用OnChanges

我们可以使用DoCheck钩子来跟踪组件本身属性以及计算值属性。由于此钩子在当前组件上的Angular进程属性发生更改之前触发,因此我们可以根据需要执行任何操作,以便在UI中正确反映更改。

我们可以使用OnInit钩子来监听Angular生态系统之外的第三方插件,并手动运行变更检测。

例如,我们有一个显示当前时间的组件。时间由Time服务提供。下面是它将如何在Angular.js中实现的:

function link(scope, element) {
    scope.$watch(() => {
        return Time.getCurrentTime();
    }, (value) => {
        $scope.time = value;
    })
}

以下是在Angular中的实现:

class TimeComponent {
    ngDoCheck()
    {
        this.time = Time.getCurrentTime();
    }
}

另一个例子是,如果我们有第三方slider组件未集成到Angular生态系统中,但我们需要显示当前幻灯片,我们只需将此组件包装到角度组件中,changed 手动跟踪滑块的事件并手动触发摘要以反映UI中的更改:

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();
        })
    }
}

译者总结:

本文作者主要阐述了一个事实:digest依旧存在于Angular中,只是内部实现的方式有所不同。区别可以概括为一下几点:

  • 由于采用单项数据流,使得Angular可以自上而下的树型检测而不是像Angular.js那样需要循环检测(如下图所示),提升了执行效率(Angular.js需要循环检测10次,Angular只要一次),更多内容可以参考Victor Savkin在ng-conf的演讲,需翻墙

image

  • Angular使用zone.js接管了所有异步事件,使得脏值检测在异步事件中也无需手动触发。

译者注:

  1. 不可变对象
  2. 关于microtask,可以点击event loop查看,在Angular源码中,触发变化检测的基本流程是这样的:
    ngZone监听onMicrotaskEmpty事件,如果事件触发则执行tick(),摘录源码如下:

    this._zone.onMicrotaskEmpty.subscribe(
        {next: () => { this._zone.run(() => { this.tick(); }); }});

    之后tick()在循环所有视图,并依此调用detectChanges

    tick(): void {
    
        if (this._runningTick) {
          throw new Error('ApplicationRef.tick is called recursively');
        }
    
        const scope = ApplicationRef._tickScope();
        try {
          this._runningTick = true;
          this._views.forEach((view) => view.detectChanges());
          if (this._enforceNoNewChanges) {
            this._views.forEach((view) => view.checkNoChanges());
          }
        } catch (e) {
          // Attention: Don't rethrow as it could cancel subscriptions to Observables!
          this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
        } finally {
          this._runningTick = false;
          wtfLeave(scope);
        }
      }
  3. 单项数据流。

    什么是单项数据流?顾名思义数据的流向是单一方向的,即数据是按照Model->Component->View的顺序流动的,这么做能够有效的保证数据的统一,Angular正是采用了这个原则,才能减少变化检测的次数。

    那么是不是意味着在Angular中就无法通过组件改变数据结构呢?显然不是这样的,只要在Angular开始渲染视图之前,改变都是允许的。

    举个例子:

    假设我们有一个组件cd,定义如下:

    import { 
        Component,
        OnChanges,
        OnInit,
        DoCheck,
        AfterContentInit,
        AfterContentChecked,
        AfterViewInit,
        AfterViewChecked
    } from '@angular/core';
    
    
    @Component({
        selector: 'cd',
        template: `
            <span>计数器:{{count}}</span>
            <ng-content></ng-content>
        `
    })
    export class CdComponent implements OnChanges,OnInit,DoCheck,AfterContentInit,AfterContentChecked,AfterViewInit,AfterViewChecked{
    
        //计数器
        count: number = 0;
    
        constructor(){}
    
        ngOnChanges(){ }
        ngOnInit(){ }
        ngDoCheck(){ }
        ngAfterContentInit(){ }
        ngAfterContentChecked(){ }
        ngAfterViewInit(){ }
        ngAfterViewChecked(){ }
    }

    假设现在要改变计数器的数值,实现的方式有很多种,在这里为了得到想要的结果,对比以下两种方式:

    在生命周期DoCheck这个钩子中实现:

    ngDoCheck(){ ++this.count; }

    在生命周期AfterViewInit这个钩子中实现:

    ngAfterViewInit(){ ++this.count; }

    两种方式都实现了数据的修改,但是第二种方式Angular会抛出错误提示,因为此时视图已经初始化完成,Angular不允许再修改数据。


http://www.niftyadmin.cn/n/1965494.html

相关文章

【394天】我爱刷题系列153(2018.03.06)

(一只心中无码的程序员)专栏 叨叨两句 ~SQL习题048 将titles_test表名修改为titles_2017。CREATE TABLE IF NOT EXISTS titles_test (id int(11) not null primary key,emp_no int(11) NOT NULL,title varchar(50) NOT NULL,from_date date NOT NULL,to_date date DEFAULT NULL…

Android应用安全

一、前言 互联网时代&#xff0c;移动应用已经进入到大众生活的各个方面,娱乐、出行、金融、支付等等&#xff0c;应用中包含了用户的各种隐私数据&#xff0c;如聊天记录&#xff0c;金融账户等等敏感数据&#xff0c;以及一些涉及用户个人财产安全的交易支付操作等&#xff…

Ubuntu使用Jenkins配置自动化打包Android APK

一、前言 我们开发好功能之后&#xff0c;需要编译打包&#xff0c;打包好的可执行程序需要交给测试人员进行测试&#xff0c;但是往往我们的项目大了之后&#xff0c;编译整个项目&#xff0c;打包的过程的时间相对比较长&#xff0c;这个时候如果是在开发人员的电脑上进行代…

与Waymo对簿公堂的Uber自动驾驶长途货车,Uber Freight终于亮相

要知道&#xff0c;Uber与从谷歌独立出来的Waymo之间的官司还未结束。 上周末&#xff0c;Uber CEO卡兰尼克过的肯定十分充实&#xff0c;因为通过社交平台不难看出&#xff0c;他和汤姆布拉迪在肯塔基赛马会上玩乐之后在匹兹堡稍作停留&#xff0c;还拍下一台侧身印着“Uber …

MyBatis动态设置要连接的数据库地址,用户名,密码

一、前言 使用Mybatis连接数据库&#xff0c;可能一般我们都是在MyBatis的全局配置文件中去进行配置要连接数据库的url,用户名&#xff0c;密码&#xff0c;但是我们有的时候会有需要动态设置要连接的数据库url&#xff0c;用户名&#xff0c;密码的需求&#xff0c;比如我们可…

TCP/IP学习(30)——L2数据链路层的数据包处理详细流程

原文地址&#xff1a;TCP/IP学习(30)——L2数据链路层的数据包处理详细流程 作者&#xff1a;GFree_Wind 本文的copyleft归gfree.windgmail.com所有&#xff0c;使用GPL发布&#xff0c;可以自由拷贝&#xff0c;转载。但转载请保持文档的完整性&#xff0c;注明原作者及原链接…

一次性搞清楚equals和hashCode

在程序设计中&#xff0c;有很多的“公约”&#xff0c;遵守约定去实现你的代码&#xff0c;会让你避开很多坑&#xff0c;这些公约是前人总结出来的设计规范。 Object类是Java中的万类之祖&#xff0c;其中&#xff0c;equals和hashCode是2个非常重要的方法。 这2个方法总是被…

硬科技企业大放异彩,Nibiru、柔宇斩获国家优质投资项目特别奖

2016—2017年度国家优质投资项目推介表彰大会在京举行。 近日&#xff0c;由国家发展改革委主管&#xff0c;中国投资协会主办的2016—2017年度国家优质投资项目推介表彰活动的审定结果在北京钓鱼台国宾馆揭晓。中国投资协会会长杨庆蔚&#xff0c;中国投资协会副会长、创投委…