【js-09】

如何理解angular的脏检查?

分享人:陈星宇

目录

1.背景介绍

2.知识剖析

3.常见问题

4.解决方案

5.编码实战

6.扩展思考

7.参考文献

8.更多讨论

1.背景介绍

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

2.知识剖析

脏检查如何触发?

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

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

实际上$apply 其实就是$digest的一个简单封装。

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

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

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

脏检查的范围

前面说到:angular 会对所有绑定到 UI 上的表达式做脏检查。其实,在 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() 这段代码后是否应该刷新 UI,应该更新哪个 DOM 元素? angular 必须去挨个检查这些元素对应绑定表达式的值是否有被改变。这就是脏数据检查的由来。

3.常见问题

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

1.触发UI事件
2.ajax请求或者
3.timeout 延迟

脏检查慢吗?

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

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

不要连接太长的 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 表达式也减少至四分之一,提升脏检查循环的速度

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!
请给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 性能优化

    8.更多讨论

    鸣谢

    感谢大家观看

    BY : 陈星宇