【js-09】

如何理解angular的脏检查?

分享人:陈皓宇

目录

1.背景介绍

2.知识剖析

3.常见问题

4.解决方案

5.编码实战

6.扩展思考

7.参考文献

8.更多讨论

1.背景介绍

Angular 是一个 MVVM 前端框架,提供了双向数据绑定。所谓双向数据绑定(Two-way data binding)就是页面元素变化会触发 View-model 中对应数据改变, 反过来 View-model 中数据变化也会引发所绑定的 View 元素数据更新。操作数据就等同于操作 View。

2.知识剖析

Angular什么时候会自动触发脏检查

Angular所系统的方法中,都会触发比较事件(脏检查),eg,controller初始化的时候,所有以ng-**的时间执行后,都会触发脏检查;

脏检查如何触发?

Angular 每一个绑定到View的数据,就会有一个 $watch 对象。

所有的watch存储在$scope的$$watchList属性中,一次脏检查就是调用一次 $apply() 或者 $digest(),遍历检查所有watch,将数据中最新的值呈现在界面上。

关于$apply(),en...实际上$apply()其实就是$digest()的一个简单封装。

我们来看看$apply的源码

                                    
                    $apply: function(expr) {
                    try {
                    beginPhase('$apply');
                    return this.$eval(expr);
                    } catch (e) {
                    $exceptionHandler(e);
                    } finally {
                    clearPhase();
                    try {
                    $rootScope.$digest();//会再次出发$digest()
                    } catch (e) {
                    $exceptionHandler(e);
                    throw e; }
                    }
                    }
                

                

比$digest多了一次$eval(),检测表达式,如果有报错会抛出异常

通常写代码时我们无需主动调用 $apply 或 $digest 是因为 angular 在外部对我们的回调函数做了包装。例如常用的 ng-click,这是一个指令(Directive),内部实现则 类似 于

                        
                            DOM.addEventListener('click', function ($scope) {
                                $scope.$apply(() => userCode());
                            });
                        
                

可以看到:ng-click 帮我们做了 $apply 这个操作。类似的不只是这些事件回调函数,还有 $http、$timeout 等。

脏检查的范围

前面说到:angular 会对所有绑定到 View 上的表达式做脏检查。其实,在 angular 实现内部,所有绑定表达式都被转换为 $scope.$watch()。 每个 $watch 记录了上一次表达式的值。有 ng-bind="a" 即有 $scope.$watch('a', callback),而 $scope.$watch 可不会管被 watch 的表达式是否跟触发脏检查的事件有关。

                    
                         

Click Me

用户点击了 span,angular 执行了一个叫 onClick 的方法。这个 onClick 的方法体对于 angular 来说是黑盒, 它到底做了什么不知道。可能改了 $scope.content1 的值,可能改了 $scope.content2 的值,也可能两个值都改了, 也可能都没改。 那么 angular 到底应该怎样得知 onClick() 这段代码后是否应该刷新 View,应该更新哪个 DOM 元素? angular 必须去挨个检查这些元素对应绑定表达式的值是否有被改变。这就是脏数据检查的由来。

3.常见问题

触发脏检查的方式有几种?

1、controller 初始化
2、几乎所有ng-开头的事件(ng-click,ng-change...)
3、http请求
4、$timeout,$interval
5、手动调用$apply(), $digest()

脏检效率如何?

脏检查效率其实是不高,但是也谈不上有多慢。简单的数字或字符串比较能有多慢呢?十几个表达式的脏检查可以直接忽略不计;上百个也可以接受; 成百上千个就有很大问题了。绑定大量表达式时请注意所绑定的表达式效率。相对于现在的流行的vue和react,ng1的效率就有些低了。

表达式(以及表达式所调用的函数)中少写太过复杂的逻辑

不要连接太长的 filter(往往 filter 里都会遍历并且生成新数组)

不要访问 DOM 元素。

4.解决方案(如何提高脏检查的效率)

1.使用单次绑定减少绑定表达式数量

单次绑定(One-time binding) 是 Angular 1.3 就引入的一种特殊的表达式,它以 :: 开头,当脏检查发现这种表达式的值不为 undefined 时就认为此表达式已经稳定, 并取消对此表达式的监视。这是一种行之有效的减少绑定表达式数量的方法,与 ng-repeat 连用效果更佳(下文会提到),但过度使用也容易引发 bug。

2.善用 NG-IF 减少绑定表达式的数量

如果你认为 ng-if 就是另一种用于隐藏、显示 DOM 元素的方法你就大错特错了。ng-if 不仅可以减少 DOM 树中元素的数量(而非像 ng-hide 那样仅仅只是加个 display: none),每一个 ng-if 拥有自己的 scope,ng-if 下面的 $watch 表达式都是注册在 ng-if 自己 scope 中。当 ng-if 变为 false,ng-if 下的 scope 被销毁,注册在这个 scope 里的绑定表达式也就随之销毁了。

举个例子:
                    
                        
  • Tab 1 title
  • Tab 2 title
  • Tab 3 title
  • Tab 4 title
[[Tab 1 body...]]
[[Tab 2 body...]]
[[Tab 3 body...]]
[[Tab 4 body...]]

对于这种会反复隐藏、显示的元素,通常人们第一反应都是使用 ng-show 或 ng-hide 简单的用 display: none 把元素设置为不可见。

然而入上文所说,肉眼不可见不代表不会跑脏检查。如果将 ng-show 替换为 ng-if 或 ng-switch-when

                    
                        
[[Tab 1 body...]]
[[Tab 2 body...]]
[[Tab 3 body...]]
[[Tab 4 body...]]

这么做有如下优点:

1.DOM 树中的元素个数显著减少至四分之一,降低内存占用

2.$watch 表达式也减少至四分之一,提升脏检查循环的速度

                    
                        
[[Tab 1 body...]]
[[Tab 2 body...]]
[[Tab 3 body...]]
[[Tab 4 body...]]

3.若tab下面有controller,那么仅当这个 tab 被选中时该 controller 才会执行,可以减少各页面的互相干扰

4.如果 controller 中调用接口获取数据,那么仅当对应 tab 被选中时才会加载,避免网络拥挤

这么做也有缺点

1.DOM 重建本身费时间
2.如果 tab 下有 controller,那么每次该 tab 被选中时 controller 都会被执行
3.如果在 controller 里面调接口获取数据,那么每次该 tab 被选中时都会重新加载

5.代码实战

6.拓展思考

当脏检查遇上ng-repeat

牵一发而动全身
不恰当的 ng-repeat 会造成 DOM 树反复重新构造,拖慢浏览器响应速度,造成页面闪烁。除了上面这种比较极端的情况,如果一个列表频繁拉取 Server 端数据自刷新的话也一定要手工添加 track by,因为接口给前端的数据是不可能包含 $$hashKey 这种东西的,于是结果就造成列表频繁的重建。
请给 ng-repeat 手工添加 track by!

解决方案

用track by

track by 与 单次绑定 连用

track by 只是让 angular 复用已有 DOM 元素。数组每个子元素内部绑定表达式的脏检查还是免不了的。然而对于实际应用场景,往往是数组整体改变(例如分页),数组每一项通常却不会单独变化。这时就可以通过使用单次绑定大量减少 $watch 表达式的数量。例如

                
                    
  • a:
    b:
    c:
    d:
    e:
  • 除非 track by 字段改变造成的 DOM 树重建,item.a 等一旦显示在页面上后就不会再被监视。

    7.参考文献

    angularjs脏检查机制 Angular 1 深度解析:脏数据检查与 angular 性能优化 再谈angularJS数据绑定机制及背后原理—angularJS常见问题总结

    8.更多讨论

    鸣谢

    感谢大家观看

    BY : 陈皓宇|陈星宇