Angularjs实践之优化你的$watch
阅读原文时间:2021年04月20日阅读:1

Angularjs的双向绑定机制带来了思维方式的转变:不再是DOM驱动,而是以Model为核心.

双向绑定机制能为我们提供很多方便的功能,但这种双向绑定需要我们时刻监控着整个页面的变化,这便是Angularjs中的脏检测机制$digest.

由此我们不难得到此结论:越多的绑定的值需要监控,耗费在监控上的资源就越多,这也是往往导致我们Angular应用效果差的原因之一,在此我重点探讨如何去优化我们的应用,如何通过优化$watch list来提升我们应用的性能.

脏数据检查 != 轮询检查更新

常见的对ng脏检查的理解是是定时轮询去检查model是否变更.

其实ng只有在指定事件触发后,才进入$digest cycle

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( timeout, interval )
  • 执行 digest()或 apply()

提速 digestcycle是提升网页性能的关键点,而提速的途径一般是减少和尽快完成 digest,网页中的 watch便是影响 digest的关键因素.

哪些操作会产生$watch?

  • Angularjs中的表达式,如{ { text } }
  • Angularjs中的大部分指令,如ngRepeat,ngBind,ngShow/ngHide,ngIf
  • 显式使用$watch函数监控数据变化

下面讨论在这些产生$watch的操作如何进行优化

开发过程中有时为了实现某些业务功能,免不了需要显示使用 scope. watch函数来监控某些值的变化.

使用方法如: scope. watch(watchExpression, modelChangeCallback) , watchExpression可以是String或Function.

及时销毁代码中的$watch

watch函数执行后会返回一个释放这个 watch的函数,对于那些不需要再使用到的$watch,尽早释放也会是一个不错的实践.

代码如下:

var unwatch = $scope.$watch("someKey", function(newValue, oldValue){
  //do sth...
  if(someCondition){
    //当不需要的时候,及时移除watch
    unwatch();
  }
});

避免深度watch, 即第三个参数为true

watch的第三个参数设置为true,即可deepwatch.不过有时候其实不想或者不需要监听collection的全部属性.只要监视其中的一个或者几个,这时候通过for循环虽然可以循环 watch不过明显太挫.

通过下面这种写法就可以监控一个collection的单独一个object属性.

$scope.people = [
    {
        "groupname": "g1", 
        "persions": [
            {
                "id": 1, 
                "name": "bill"
            }, 
            {
                "id": 2, 
                "name": "bill2"
            }
        ]
    }, 
    {
        "groupname": "g2", 
        "persions": [
            {
                "id": 3, 
                "name": "bill3"
            }, 
            {
                "id": 4, 
                "name": "bill4"
            }
        ]
    }
]

$scope.$watch(function($scope) {
    return $scope.people.map(function(obj) {
        return obj.persions.map(function(g){
            return g.name
        });
    });
}, function (newVal) {
    $scope.count++;
    $scope.msg = 'person name was changed'+ $scope.count;
}, true);

减少watch的变量长度

如下,angular不会仅对 {{value}} 建立watcher,而是对整个p标签. 双括号应该被span包裹,因为watch的是外部element

<p>plain text other {{value}} plain text other</p>
//改为:
<p>plain text other <span ng-bind='value'></span> plain text other</p>
//或
<p>plain text other <span>{{value}}</span> plain text other</p>

避免watchExpression中执行耗时操作

避免watchExpression中执行耗时操作,因为它在每次$digest都会执行1~2次.

避免watchExpression中操作dom.

同上,并且在此时操作DOM的价值是昂贵的.

大部分时候可能我们会在watch中操作DOM是必须的,这里提到的应该是要尽量快速少量的去操作这些DOM.

ngIf/ngShow ,前者会移除DOM和对应的$watch.

进行单次绑定(BindOnce)

如下面的一段代码:

html代码

<ul data-ng-controller="myController">
    <li ng-repeat="people in peoples">
        <div> {{people.name}} </div>
        <div> {{people.age}} </div>
    </li>
</ul>

js代码

angular.module('app').controller('myController', function($scope) {
  $scope.peoples = [...];
});

假设以上代码中的 scope.peoples中有1000个数据,则这个页面将会产生1000∗2+1=2001个 watch,由于Angularjs的双向绑定机制,$digest会时刻监控页面中这些值的变化,很轻易的便能打到Angularjs的性能瓶颈(在社区中有这么一个经验,如果超过2000个watcher,就可能感觉到明显的卡顿,特别在IE8这种老旧浏览器上),像这种列表式的数据展示,我们通常是经常会用到的,而这些数据有往往只是一些加载进来的静态资源,双向绑定在展示上毫无用处可言,这时如果我们如果能单次绑定这些数据而不实时去监控它们的变化,网页性能是不是也一样会有所提高?

在Angularjs1.3+的版本中为Angular表达式{ { } }引入了新语法,以“::”作为前缀的表达式为单次绑定。(单次表达式在第一次$digest完成后,将不再计算(监测属性的变化))

对于上面的例子可以改为:

<ul data-ng-controller="myController">
  <li ng-repeat="people in peoples">
    <div> {{::people.name}} </div>
    <div> {{::people.age}} </div>
  </li>
</ul>

对于1.3之前版本则可以采用一些开源项目里面封装好的一系列指令来实现(下面以Bindonce为例)

Bindonce
Angular-once

添加依赖

angular.module('com.ngnice.app', ['pasvaz.bindonce']);

添加完依赖后便可如下使用:

<ul data-ng-controller="myController">
  <li bindonce ng-repeat="people in peoples">
    <div bo-text="people.name"></div>
    <div bo-text="people.age"></div>
  </li>
</ul>

采用单次绑定的方式,页面上的每个people对象只绑定了一个 watch, watch数量从2001个缩减到1001个.

如何检查页面$watch数量

在控制台上复制下列代码执行即可看到$watch数量:

function getWatchers(root) {
  root = angular.element(root || document.documentElement);
  var watcherCount = 0;

  function getElemWatchers(element) {
    var isolateWatchers = getWatchersFromScope(element.data().$isolateScope);
    var scopeWatchers = getWatchersFromScope(element.data().$scope);
    var watchers = scopeWatchers.concat(isolateWatchers);
    angular.forEach(element.children(), function (childElement) {
      watchers = watchers.concat(getElemWatchers(angular.element(childElement)));
    });
    return watchers;
  }

  function getWatchersFromScope(scope) {
    if (scope) {
      return scope.$$watchers || [];
    } else {
      return [];
    }
  }

  return getElemWatchers(root);
}
getWatchers().length;

或者安装chrome下的Angularjs拓展AngularJS Batarang也可以实时查看页面的$watch数量.

总结

Angularjs的双向绑定机制是建立在 digest脏检测基础上的,而 watch的数量是影响 digest的主要因素之一,在实践中移除不必要的 watch对应用性能的提升不容小觑,当页面上的$watch数量过多时,思考如果减少watch数量将会是一个不错的方向.

有关Angularjs中$digest过程中的优化还有其他需要注意的方面,有时间再一一整理.

参考资料

破狼-angularjs移除不必要的$watch

Angular性能优化心得