Contents
  1. 1. ng只有在指定事件触发后,才进入$digest cycle:
  2. 2. 假如没有AngularJS该如何实现双向绑定的效果
  3. 3. $digest和$apply

angular并不存在定时脏检测。angular是基于事件触发,遍历rootscope, 检查所有的watcher。读 徐飞的双向绑定 有感

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

  1. DOM事件,譬如用户输入文本,点击按钮等。(ng-click)
  2. XHR响应事件 ($http)
  3. 浏览器Location变更事件 ($location)
  4. Timer事件($timeout, $interval)

才会执行$digest()或$apply()。

假如没有AngularJS该如何实现双向绑定的效果

    
    <button ng-click="inc">increase 1</button>
        <button ng-click="inc2">increase 2</button>
        <span style="color:red" ng-bind="counter"></span>
        <span style="color:blue" ng-bind="counter"></span>
        <span style="color:green" ng-bind="counter"></span>

<script type="text/javascript">
    /* 数据模型区开始 */
    var counter = 0;

    function inc() {
        counter++;
    }

    function inc2() {
        counter+=2;
    }
    /* 数据模型区结束 */

    /* 绑定关系区开始 */
    function init() {
        bind();
    }

    function bind() {
        var list = document.querySelectorAll("[ng-click]");
        for (var i=0; i<list.length; i++) {
            list[i].onclick = (function(index) {
                return function() {
                    window[list[index].getAttribute("ng-click")]();
                    apply();
                };
            })(i);
        }
    }

    function apply() {
        var list = document.querySelectorAll("[ng-bind='counter']");
        for (var i=0; i<list.length; i++) {
            list[i].innerHTML = counter;
        }
    }
    /* 绑定关系区结束 */
</script>

可以看到,在这么一个简单的例子中,我们做了一些双向绑定的事情。从两个按钮的点击到数据的变更,这个很好理解,但我们没有直接使用DOM的onclick方法,而是搞了一个ng-click,然后在bind里面把这个ng-click对应的函数拿出来,绑定到onclick的事件处理函数中。为什么要这样呢?因为数据虽然变更了,但是还没有往界面上填充,我们需要在此做一些附加操作。

从另外一个方面看,当数据变更的时候,需要把这个变更应用到界面上,也就是那三个span里。但由于Angular使用的是脏检测,意味着当改变数据之后,你自己要做一些事情来触发脏检测,然后再应用到这个数据对应的DOM元素上。问题就在于,怎样触发脏检测?什么时候触发?

我们知道,一些基于setter的框架,它可以在给数据设值的时候,对DOM元素上的绑定变量作重新赋值。脏检测的机制没有这个阶段,它没有任何途径在数据变更之后立即得到通知,所以只能在每个事件入口中手动调用apply(),把数据的变更应用到界面上。在真正的Angular实现中,这里先进行脏检测,确定数据有变化了,然后才对界面设值。

所以,我们在ng-click里面封装真正的click,最重要的作用是为了在之后追加一次apply(),把数据的变更应用到界面上去。

那么,为什么在ng-click里面调用$digest的话,会报错呢?因为Angular的设计,同一时间只允许一个$digest运行,而ng-click这种内置指令已经触发了$digest,当前的还没有走完,所以就出错了。

$digest和$apply

在Angular中,有$apply和$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上。但这个时候,也可以不用$digest,而是使用$apply,效果是一样的,那么,它们的差异是什么呢?

最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。

    

        var app = angular.module("test", []);

        app.directive("myclick", function() {
            return function (scope, element, attr) {
                element.on("click", function() {
                    scope.counter++;
                    scope.$apply(function() {
                        scope.counter++;
                    });
                });
            };
        });

        app.controller("CounterCtrl", function($scope) {
            $scope.counter = 0;
        });

    

在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。

对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例

    
    var app = angular.module("test", []);

    app.directive("increasea", function() {
        return function (scope, element, attr) {
            element.on("click", function() {
                scope.a++;
                scope.$digest();
            });
        };
    });

    app.directive("increaseb", function() {
        return function (scope, element, attr) {
            element.on("click", function() {
                scope.b++;
                scope.$digest();    //这个换成$apply即可
            });
        };
    });

    app.controller("OuterCtrl", ["$scope", function($scope) {
        $scope.a = 1;

        $scope.$watch("a", function(newVal) {
            console.log("a:" + newVal);
        });

        $scope.$on("test", function(evt) {
            $scope.a++;
        });
    }]);

    app.controller("InnerCtrl", ["$scope", function($scope) {
        $scope.b = 2;

        $scope.$watch("b", function(newVal) {
            console.log("b:" + newVal);
            $scope.$emit("test", newVal);
        });
    }]);

    <div ng-app="test">
        <div ng-controller="OuterCtrl">
            <div ng-controller="InnerCtrl">
                <button increaseb>increase b</button>
                <span ng-bind="b"></span>
            </div>
            <button increasea>increase a</button>
            <span ng-bind="a"></span>
        </div>
    </div>
    

这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。

因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控

从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。

Contents
  1. 1. ng只有在指定事件触发后,才进入$digest cycle:
  2. 2. 假如没有AngularJS该如何实现双向绑定的效果
  3. 3. $digest和$apply