MINIBASE

How does data binding work in AngularJS

photo by Krzysztof Kowalik on unsplash.com

AngularJS 可以记住 value 值并且会把它和之前的 value 值进行比较。这就是基本的脏检查机制。如果某处的value 值发生了变化,那么 AngularJS 就会触发指定事件。

$apply()这个方法是用来处理AngularJS框架之外的表达式的,与它相辅相成的还有$digest()方法。一次digest就是一次完全的脏检查,它可以运行在所有的浏览器中。

关于$watch

每一次你在UI中绑定什么东西时你就会往$watch的队列中插入一条$watch,想象一下$watch就是在所监测的model中可以侦查数据变化的东西。比如说:

User: <input type="text" ng-model="user" />
Password: <input type="password" ng-model="pass" />

在这里我们分别给两个input绑定了$scope.user$scope.pass,就是说我们向$watch队列添加了两个$watch

每一个绑定到了UI上的数据都会生成一个$watch,我们的模板加载完成时,也就是在linking阶段,Angular解释器会寻找每一个directive并且创造它们所需的$watch

一个watcher包含了三个东西:

  • 它正在监听的表达式。有可能是一个简单的属性名,也有可能是更复杂的东西

  • 这个表达式目前已知的value值,它会与当前正在计算的表达式value值进行核对比较,如果监听到value值发生了改变将会触发函数并把$scope标记为dirty

  • 被触发执行的函数

$$watchers = [
    {
        eq: false, // 表明我们是否需要检查对象级别的相等
        fn: function( newValue, oldValue ) {}, // 这是我们提供的监听器函数
        last: 'Ryan', // 变量的最新值
        exp: function(){}, // 我们提供的watchExp函数
        get: function(){} // Angular's编译后的watchExp函数
    }
];

定义监听器的几种方法: 1.把$watch设置为$scope的一种属性:$scope.$watch('person.username', validateUnique); 2.插入angular表达式:<p>username: {{person.username}}</p> 3.使用类似于ng-model的指令来定义监听器:<input ng-model="person.username />

关于$digest$apply

如果你点击一个按钮,或者在一个input框中输入,事件的回调函数会在javascript中运行,并且你可以做任意的DOM操作,当回调函数结束时,浏览器会相应地在DOM中做出改变。

当一个控制器/指令/等等东西在AngularJS中运行时,AngularJS内部会运行一个叫做$scope.$apply的函数。这个$apply函数会接收一个函数作为参数并运行它,在这之后才会在rootScope上运行$digest函数。

AngularJS的$apply函数代码如下所示:

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

由此可见,使用$apply可带参数。

$digest函数将会在$rootScope中被$scope.$apply所调用。它将会在$rootScope中运行digest循环,然后向下遍历每一个作用域并在每个作用域上运行循环。在简单的情形中,digest循环将会触发所有位于$$watchers变量中的所有watchExp函数,将它们和最新的值进行对比,如果值不相同,就会触发监听器。$digest函数检查$watch队列中的所有监听器最新的value值,一次$digest循环是被指令触发的。如果表达式新的value值与之前不同,就会调用监听器的函数,这个函数可能是重新编译部分的DOM,重新计算$scope的值,激活一个AJAX请求,或者任何你想做的事。

监听器函数可以修改$scope或是父$scope的其他属性,一旦有出发了一个监听器函数,我们就无法保证其它的$scope也是干净的,所以我们会再次执行整个digest循环。

$apply$digest作用类似,$apply会使ng进入$digest cycle, 并从$rootScope开始遍历(深度优先)检查数据变更。不同之处在于$apply可以带参数,并且会触发作用域上的所有监控,$digest仅仅触发当前作用域和子作用域的监控。

build your own dirty-checking

了解以上知识后,我们可以自己写一个具有基本功能的脏检测了。 首先定义Scope,然后扩展这个函数的原型对象来复制$digest和$watch

var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( ) {

};

Scope.prototype.$digest = function( ) {

};

设置$watch函数,它接收watchExp和listener这两个参数,被调用时我们会把其push到$$watchers数组中。因此代码扩展为:

Scope.prototype.$watch = function( watchExp, listener ) {
    this.$$watchers.push( {
        watchExp: watchExp,
        listener: listener || function() {}
    } );
};

如果没有传入listener的话我们会把它设置为空函数。 $digest用来检查新值旧值是否相等,如果不相等则触发监听器,不断循环这个过程,直到新值旧值相等。

Scope.prototype.$digest = function( ) {
    var dirty;

    do {
            dirty = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newValue = this.$$watchers[i].watchExp(),
                    oldValue = this.$$watchers[i].last;

                if( oldValue !== newValue ) {
                    this.$$watchers[i].listener(newValue, oldValue);

                    dirty = true;

                    this.$$watchers[i].last = newValue;
                }
            }
    } while(dirty);
};

下一步我们需要创建一个作用域的实例,并把实例赋值给$scope,然后注册监听函数,使得更新$scope之后运行$digest

var $scope = new Scope();

$scope.name = 'Ryan';

$scope.$watch(function(){
    return $scope.name;
}, function( newValue, oldValue ) {
    console.log(newValue, oldValue);
} );

$scope.$digest();

我们发现在控制台输出了Ryan undefined,成功了! 最后我们可以把$digest函数绑定到事件上,比如input元素的keyup事件,即意味着我们可以实现双向数据绑定!

var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( watchExp, listener ) {
    this.$$watchers.push( {
        watchExp: watchExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirty;

    do {
            dirty = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newValue = this.$$watchers[i].watchExp(),
                    oldValue = this.$$watchers[i].last;

                if( oldValue !== newValue ) {
                    this.$$watchers[i].listener(newValue, oldValue);

                    dirty = true;

                    this.$$watchers[i].last = newValue;
                }
            }
    } while(dirty);
};


var $scope = new Scope();

$scope.name = 'Ryan';

var element = document.querySelectorAll('input');

element[0].onkeyup = function() {
    $scope.name = element[0].value;

    $scope.$digest();
};

$scope.$watch(function(){
    return $scope.name;
}, function( newValue, oldValue ) {
    console.log('Input value updated - it is now ' + newValue);

    element[0].value = $scope.name;
} );

var updateScopeValue = function updateScopeValue( ) {
    $scope.name = 'Bob';
    $scope.$digest();
};

参考内容