分享人:陈皓宇
目录
1.背景介绍
2.知识剖析
3.常见问题
4.解决方案
5.编码实战
6.扩展思考
7.参考文献
8.更多讨论
Angular 是一个 MVVM 前端框架,提供了双向数据绑定。所谓双向数据绑定(Two-way data binding)就是页面元素变化会触发 View-model 中对应数据改变, 反过来 View-model 中数据变化也会引发所绑定的 View 元素数据更新。操作数据就等同于操作 View。
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 必须去挨个检查这些元素对应绑定表达式的值是否有被改变。这就是脏数据检查的由来。
1、controller 初始化
2、几乎所有ng-开头的事件(ng-click,ng-change...)
3、http请求
4、$timeout,$interval
5、手动调用$apply(), $digest()
脏检查效率其实是不高,但是也谈不上有多慢。简单的数字或字符串比较能有多慢呢?十几个表达式的脏检查可以直接忽略不计;上百个也可以接受; 成百上千个就有很大问题了。绑定大量表达式时请注意所绑定的表达式效率。相对于现在的流行的vue和react,ng1的效率就有些低了。
表达式(以及表达式所调用的函数)中少写太过复杂的逻辑
不要连接太长的 filter(往往 filter 里都会遍历并且生成新数组)
不要访问 DOM 元素。
单次绑定(One-time binding) 是 Angular 1.3 就引入的一种特殊的表达式,它以 :: 开头,当脏检查发现这种表达式的值不为 undefined 时就认为此表达式已经稳定, 并取消对此表达式的监视。这是一种行之有效的减少绑定表达式数量的方法,与 ng-repeat 连用效果更佳(下文会提到),但过度使用也容易引发 bug。
如果你认为 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 被选中时都会重新加载
牵一发而动全身
不恰当的 ng-repeat 会造成 DOM 树反复重新构造,拖慢浏览器响应速度,造成页面闪烁。除了上面这种比较极端的情况,如果一个列表频繁拉取 Server 端数据自刷新的话也一定要手工添加
track by,因为接口给前端的数据是不可能包含 $$hashKey 这种东西的,于是结果就造成列表频繁的重建。
请给 ng-repeat 手工添加 track by!
用track by
track by 只是让 angular 复用已有 DOM 元素。数组每个子元素内部绑定表达式的脏检查还是免不了的。然而对于实际应用场景,往往是数组整体改变(例如分页),数组每一项通常却不会单独变化。这时就可以通过使用单次绑定大量减少 $watch 表达式的数量。例如
a:
b:
c:
d:
e:
除非 track by 字段改变造成的 DOM 树重建,item.a 等一旦显示在页面上后就不会再被监视。
感谢大家观看
BY : 陈皓宇|陈星宇