angular的双向绑定原理

http://sentsin.com/web/779.html


AngularJS是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。

一.什么是数据双向绑定
Angular实现了双向绑定机制。所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。

一个最简单的示例就是这样:

?
1
2
3
4
5
6
< div ng-controller = "CounterCtrl" >
   < span ng-bind = "counter" > span >
   < button ng-click = "counter++" >increase button >
div >function CounterCtrl($scope) {
   $scope.counter = 1;
}

这个例子很简单,每当点击一次按钮,界面上的数字就增加一。

二.数据双向绑定原理
1.深入理解
实现用户控制手机列表显示顺序的特性。动态排序可以这样实现,添加一个新的模型属性,把它和迭代器集成起来,然后让数据绑定完成剩下的事情。

模板(app/index.html)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Search: "query" >
Sort by:
 
 
    "phones" >
 
  • "phone in phones | filter:query | orderBy:orderProp" >
  •    {{phone.name}}
      

    {{phone.snippet}}

     

    在index.html中做了如下更改:

    首先,增加了一个叫做orderProp的 "number" data-bind-123= "name" />

    这样输入值会自动映射到user对象的name属性,反之亦然。到此这个简单实现就完成了。

    四.Angular实现数据双向绑定
    Angular主要通过scopes实现数据双向绑定。AngularJS的scopes包括以下四个主要部分:

    digest循环以及dirty-checking,包括watch,digest,和$apply。
    Scope继承 - 这项机制使得我们可以创建scope继承来分享数据和事件。
    对集合 – 数组和对象 – 的有效dirty-checking。
    事件系统 - on,emit,以及$broadcast。

    我们主要讲解第一条Angular数据绑定是怎么实现的。

    1.digest循环以及dirty-checking,包括watch,digest,和$apply
    ①浏览器事件循环和Angular.js扩展
    我们的浏览器一直在等待事件,比如用户交互。假如你点击一个按钮或者在输入框里输入东西,事件的回调函数就会在javascript解释器里执行,然后你就可以做任何DOM操作,等回调函数执行完毕时,浏览器就会相应地对DOM做出变化。 Angular拓展了这个事件循环,生成一个有时成为angular context的执行环境(这是个重要的概念)。

    ②watch队列(watch list)
    每次你绑定一些东西到你的UI上时你就会往$watch队列里插入一条$watch。想象一下$watch就是那个可以检测它监视的model里时候有变化的东西。

    当我们的模版加载完毕时,也就是在linking阶段(Angular分为compile阶段和linking阶段---译者注),Angular解释器会寻找每个directive,然后生成每个需要的$watch。

    ③$digest循环
    还记得我前面提到的扩展的事件循环吗?当浏览器接收到可以被angular context处理的事件时,digest循环就会触发。这个循环是由两个更小的循环组合起来的。一个处理evalAsync队列,另一个处理watch队列。 这个是处理什么的呢?digest将会遍历我们的watch,然后询问它是否有属性和值的变化,直$watch队列都检查过。

    这就是所谓的dirty-checking。既然所有的$watch都检查完了,那就要问了:有没有$watch更新过?如果有至少一个更新过,这个循环就会再次触发,直到所有的$watch都没有变化。这样就能够保证每个model都已经不会再变化。记住如果循环超过10次的话,它将会抛出一个异常,防止无限循环。 当$digest循环结束时,DOM相应地变化。

    例如:controllers.js

    ?
    1
    2
    3
    4
    5
    6
    7
    app.controller( 'MainCtrl' , function () {
      $scope.name = "Foo" ;
     
      $scope.changeFoo = function () {
        $scope.name = "Bar" ;
      }
    });
    ?
    1
    2
    {{ name }}

    这里我们有一个$watch因为ng-click不生成$watch(函数是不会变的)。

    • 我们按下按钮
    • 浏览器接收到一个事件,进入angular context(后面会解释为什么)。
    • $digest循环开始执行,查询每个$watch是否变化。
    • 由于监视$scope.name的$watch报告了变化,它会强制再执行一次$digest循环。
    • 新的$digest循环没有检测到变化。
    • 浏览器拿回控制权,更新与$scope.name新值相应部分的DOM。

    这里很重要的(也是许多人的很蛋疼的地方)是每一个进入angular context的事件都会执行一个$digest循环,也就是说每次我们输入一个字母循环都会检查整个页面的所有$watch。

    ④通过$apply来进入angular context
    谁决定什么事件进入angular context,而哪些又不进入呢?$apply!

    如果当事件触发时,你调用apply,它会进入angularcontext,如果没有调用就不会进入。现在你可能会问:刚才的例子里我也没有调用apply,为什么?Angular为你做了!因此你点击带有ng-click的元素时,时间就会被封装到一个apply调用。如果你有一个ng−model="foo"的输入框,然后你敲一个f,事件就会这样调用apply("foo = 'f';")。

    Angular什么时候不会自动为我们apply呢?这是Angular新手共同的痛处。为什么我的jQuery不会更新我绑定的东西呢?因为jQuery没有调用apply,事件没有进入angular context,$digest循环永远没有执行。

    2.具体实现
    AngularJS的scopes就是一般的JavaScript对象,在它上面你可以绑定你喜欢的属性和其他对象,然而,它们同时也被添加了一些功能用于观察数据结构上的变化。这些观察的功能都由dirty-checking来实现并且都在一个digest循环中被执行。
    ①Scope 对象
    创建一个test/scope_spec.js文件,并将下面的测试代码添加到其中:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    test/scope_spec.js
    -------
    /* jshint globalstrict: true */ 
    /* global Scope: false */
    'use strict' ;
    describe( "Scope" , function () {
    it( "can be constructed and used as an object" , function () {
    var scope = new Scope();
    scope.aProperty = 1;
       expect(scope.aProperty).toBe(1);
      });
    });

    这个测试用来创建一个Scope,并在它上面赋一个任意值。我们可以轻松的让这个测试通过:创建src/scope.js文件然后在其中添加以下内容:
    src/scope.js 

    ?
    1
    2
    3
    4
    ------
    /* jshint globalstrict: true */
    'use strict' ; function Scope() {
    }

    在这个测试中,我们将一个属性(aProperty)赋值给了这个scope。这正是Scope上的属性运行的方式。它们就是正常的JavaScript属性,并没有什么特别之处。这里你完全不需要去调用一个特别的setter,也不需要对你赋值的类型进行什么限制。真正的魔法在于两个特别的函数:watch和digest。我们现在就来看看这两个函数。

    ②监视对象属性:watch和digest
    watch和digest是同一个硬币的两面。它们二者同时形成了$digest循环的核心:对数据的变化做出反应。

    为了实现这一块功能,我们首先来定义一个测试文件并断言你可以使用watch来注册一个监视器,并且当有人调用了digest的时候监视器的监听函数会被调用。

    在scope_spec.js文件中添加一个嵌套的describe块。并创建一个beforeEach函数来初始化这个scope,以便我们可以在进行每个测试时重复它:

    test/scope_spec.js  

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ------
    describe( "Scope" , function () {
    it( "can be constructed and used as an object" , function () { var scope = new Scope();
    scope.aProperty = 1;
       expect(scope.aProperty).toBe(1);
      });
    describe( "digest" , function () {
    var scope;
    beforeEach( function () { scope = new Scope();
    });
    it( "calls the listener function of a watch on first $digest" , function () { var watchFn = function () { return 'wat' ; };
    var listenerFn = jasmine.createSpy();
    scope.$watch(watchFn, listenerFn);
        scope.$digest();
        expect(listenerFn).toHaveBeenCalled();
    }); });
    });

    在上面的这个测试中我们调用了watch来在这个scope上注册一个监视器。我们现在对于监视函数本身并没有什么兴趣,因此我们随便提供了一个函数来返回一个常数值。作为监听函数,我们提供了一个JasmineSpy。接着我们调用了digest并检查这个监听器是否真正被调用。

    首先,这个Scope需要有一些地方去存储所有被注册的监视器。我们现在就在Scope构造函数中添加一个数组存储它们:

    src/scope.js

    ?
    1
    2
    3
    4
    5
    -----
     
    function Scope(){
       this .$$watchers = [];
    }

    上面代码中的$$前缀在AngularJS框架中被认为是私有变量,它们不应该在应用的外部被调用。
    现在我们可以来定义watch函数了。它接收两个函数作为参数,并且将它们储存在$watchers数组中。我们想要每一个Scope对象都拥有这个函数,因此我们将它添加到Scope的原型中:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    src/scope.js 
    -----
    Scope.prototype.$watch = function (watchFn, listenerFn) {
      var watcher = {
       watchFn: watchFn,
       listenerFn: listenerFn
      };
    this .$$watchers.unshift(watcher);
    };

    最后我们应该有一个digest函数。现在,我们来定义一个digest函数的简化版本,它仅仅只是会迭代所有的注册监视器并调用它们的监听函数:
    digest能够持续的迭代所有监视函数,直到被监视的值停止变化。多做几次digest是我们能够获得运用于监视器并依赖于其他监视器的变化。

    首先,我们新建名为$$digestOnce,并且调整它以便它能够在所有监视器上运行一遍,然后返回一个布尔值来说明有没有任何变化:
    src/scope.js

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ----
     
    Scope.prototype.$$digestOnce = function (){
       var length = this .$$watchers.length;
       var watcher, newValue, oldValue, dirty;
       while (length--){
         watcher = this .$$watchers[length];
         newValue = watcher.watchFn( this );
         oldValue= watcher.last;
         if (newValue !== oldValue){
           watcher.last == newValue;
           watcher.listenerFn(newValue, oldValue, this );
           dirty = true ;
         }
       }
        return dirty;
    };

    接着,我们重定义digest以便它能够运行“外循环”,在变化发生时调用$digestOnce:

    src/scope.js

    ?
    1
    2
    3
    4
    5
    6
    7
    -----
    Scope.prototype.$digest = function (){
       var dirty;
       do {
         dirty = this .$$digestOnce();
       } while (dirty);
    };

    以上就是Angular数据双向绑定的相关介绍,希望对大家的学习有所帮助。

    你可能感兴趣的:(angular的双向绑定原理)