【JS-9】AngularJS 双向绑定后,发生了什么事情?

小课堂163期

分享人:禚洪宇

目录

1.背景介绍

2.知识剖析

3.常见问题

4.解决方案

5.编码实战

6.扩展思考

7.参考文献

8.更多讨论

1.背景介绍

AngularJS context

正常来讲,浏览器接受到事件后会执行对应的回调函数, 这将进入 JavaScript 上下文; 一旦回调函数完成,浏览器将离开 JavaScript 上下文并根据 DOM 更改重新呈现视图, 等待更多的事件。 AngularJS 拓展了这个循环,生成一个Angular context。

2.知识剖析

2.1 $watch list

Scopes provide APIs ($watch) to observe model mutations.

                        
            User: ‹input type="text" ng-model="user"›
            Password: ‹input type="password" ng-model="pass"›
                
            $watch('user',function(){})
            $watch('pass',function(){})
                        
                    
                    
            app.controller('MainCtrl', function($scope) {
                $scope.foo = "Foo";
                $scope.world = "World";
            });
        
            html:
            Hello, {{ World }}
                    
                

也就是说,$watch方法只有在html中绑定才会创建,在controller中添加变量并不会创建

                    
        假如$scope.people中有10条数据,那么这段代码中
        会生成多少条$watch呢?

            app.controller('MainCtrl', function($scope) {
                 $scope.people = [...];
            });

            index.html:
            <'ul>
                <'li ng-repeat="person in people">
                    {{person.name}} - {{person.age}}
                <'/li>
            <'/ul>
                    
                

每一个绑定到了UI(user interface:用户界面)上的数据都会生成一个$watch

$watch是何时生成的呢?

当模板加载完毕,进入linking阶段时 (Angular分为compile阶段和linking阶段) ,Angular编译器(compiler)会寻找每个指令(directive)然后创建每个指令所需要的$watch。

$digest loop

当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发

  • evalAsync队列
  • $watch队列
  • dirty-checking
  • 例如:

                        
            controllers.js
                app.controller('MainCtrl', function() {
                    $scope.name = "Foo";
                               
                    $scope.changeFoo = function() {
                        $scope.name = "Bar";
                    }
                }); 
                
            index.html
                {{ name }}
                <'button ng-click="changeFoo()">Change the name<'/button>
                        
                    

    过程:

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

    $apply

    Scopes provide APIs ($apply) to propagate any model changes through the system into the view from outside of the "AngularJS realm" (controllers, services, AngularJS event handlers).

    谁来决定哪些事件进入angualr context,哪些不进入呢?

    当用n-click触发点击事件时,angular会将事件封装到$apply中执行

    如果有一个ng-model="foo"的input,然后敲一个f,事件就会这样调用$apply("foo = 'f';")。

    Angular什么时候不会自动为我们$apply呢?

                        
                app.directive('clickable', function() {
     
                    return {
                        restrict: "E",
                        scope: {
                            foo: '=',
                            bar: '='
                        },
                        template: '<'ul style="background-color: lightblue"><'li>{{foo}}<'/li><'li>{{bar}}<'/li><'/ul>',
                        link: function(scope, element, attrs) {
                            element.bind('click', function() {
                                scope.foo++;
                                scope.bar++;
                            });
                        }
                    }                 
                });                   
                app.controller('MainCtrl', function($scope) {
                    $scope.foo = 0;
                    $scope.bar = 0;
                });                        
                        
                    

    Tip:
    如果想使用一个jQuery插件,并且要执行$digest循环来更新DOM的话,要确保调用了$apply。

    使用$watch来监视你自己的东西

                        
                app.controller('MainCtrl', function($scope) {
                    $scope.name = "Angular";
                    $scope.updated = -1;
                    $scope.$watch('name', function() {
                        $scope.updated++;
                        //controller执行到这个$watch时,
                        //它会立即执行一次,因此我们设置updated为-1
                    });
                });
                  
                index.html
                <'body ng-controller="MainCtrl">
                    <'input ng-model="name" />
                    Name updated: {{updated}} times.
                <'/body>
                        
                    
                        
                app.controller('MainCtrl', function($scope) {
                    $scope.name = "Angular";
                    $scope.updated = 0;
                    $scope.$watch('name', function(newValue, oldValue) {
                      if (newValue === oldValue) { return; } // AKA first run
                      $scope.updated++;
                    });
                  });
                                  
                index.html
                <'body ng-controller="MainCtrl">
                    <'input ng-model="name" />
                    Name updated: {{updated}} times.
                <'/body>
                        
                    
                        
                app.controller('MainCtrl', function($scope) {
                    $scope.user = { name: "Fox" };              
                    $scope.updated = 0;                
                    $scope.$watch('user', function(newValue, oldValue) {
                      if (newValue === oldValue) { return; }
                      $scope.updated++;
                    });
                  });
                                  
                index.html
                <'body ng-controller="MainCtrl">
                    <'input ng-model="user.name" />
                    Name updated: {{updated}} times.
                <'/body>
                        
                    
                        
                app.controller('MainCtrl', function($scope) {
                    $scope.user = { name: "Fox" };
                               
                    $scope.updated = 0;
                               
                    $scope.$watch('user', function(newValue, oldValue) {
                      if (newValue === oldValue) { return; }
                      $scope.updated++;
                    }, true);
                  });
                              
                index.html
                <'body ng-controller="MainCtrl">
                    <'input ng-model="user.name" />
                    Name updated: {{updated}} times.
                <'/body>
                       
                   

    5.编码实战

    6.扩展思考

    dirty-checking是否很慢?

    7.参考文献

    参考一: $watch How the $apply Runs a $digest

    参考二: Angular 官方文档

    8.更多讨论

    感谢观看

    BY :禚洪宇