Angular的启动与双向绑定
Angular(1.2,不同版本代码结构会有差别)的启动过程
angular.js是一个自执行函数,实际上angular是一个对象。当angular.js被引入之后,执行如下代码:
//try to bind to jquery now so that one can write angular.element().read() //but we will rebind on bootstrap again. bindJQuery(); publishExternalAPI(angular); jqLite(document).ready(function() { angularInit(document, bootstrap); });
1. 绑定jQuery,如果外部引入了jQuery文件,则使用外部jQuery,如果没有引入外部jQuery文件,AngularJs内部定义了简化版的jQuery:JQLite
2. 绑定jQuery之后,执行函数publishExternalAPI
2.1 首先扩展angular对象,给对象添加一些内置的方法函数(文档中列出的angular.***方法函数)
2.2 然后定义angular.module方法,用于定义模块
2.3 使用angular.module方法定义ngLocale模块、ng模块,并注册内置的指令(directive)和服务(provider)(这些就是文档中列出的指令和服务)
3. 等待document ready之后,执行函数angularInit
3.1 首先寻找包含ng-app(或者其他几种写法)的HTML元素,如果找到,则自动执行bootstrap函数启动Angular,如果没有找到,那么需要我们在外部手动调用angular.bootstrap启动Angular
3.2 bootstrap中会生成一个注入器injector,然后执行以下代码,并最后返回一个注入器实例
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }] );
以上是Angular的启动过程,其中最后一步(以上代码)留到后面详细说明。
双向绑定
以上代码中,injector.invoke的作用是执行注入,第一个注入的是$rootScope(源码中对应的是$RootScopeProvider,在以上启动过程说明2.3中注册的服务之一)
我们知道,在Angular中,控制器中都会注入$scope,$scope上定义的属性和方法,可以直接写在页面中,Angular会自动识别。$scope相当于视图与控制器之间的连接桥梁,视图改变会更新$scope中的数据,在控制器中对$scope上的属性进行修改,也会反映到视图上,这就是我们说的双向数据绑定。
compile是Angular编译指令的过程,在这个过程中,Angular会扫描整个ng-app文档,为$scope上的属性和文档中的变量名建立观察对象,并将该观察对象加入当前scope的$$watchers数组中,观察对象如下所示,其中watchFn表示观察函数,当watchFn的返回值发生变化时,需要执行的函数为listenerFn,那么当前scope中的所有需要观察的对象都记录在了$$watchers数组中。
var watch = { watchFn: watchFn, listenerFn: listenerFn };
我们看到compile函数放在scope.apply()中执行,那么,在编译完成之后,Angular会为我们调用$digest循环,$digest循环会遍历$$watchers数组,对于数组中的每一个对象,如果watchFn的返回的新值与旧值比较发生了变化,那么就会执行listenerFn,如果listenerFn中对$scope上的数据有改变,那么$digest会再执行一次,直到数据没有改变为止(Angular默认的最大循环次数是10次)。这样就实现了数据到视图的绑定。
而对于视图到数据的过程,Angular绑定了input、textarea等的input、change事件来实现,例如,当通过键盘输入使得input的value发生改变时,会触发input事件,该事件回调中实现了对$scope上数据进行更改的操作,并调用$scope.apply()来使得$scope上的数据更改之后进入$digest循环。
下面是例子:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>双向绑定</title> <script src="jquery.js"></script> <script src="two-way-binding.js"></script> </head> <body> <input id="input" type="text"> <span id="content"></span> </body> </html>
//two-way-binding.js function Scope() { this.$$watchers = []; //记录所有观察者 } /** * Scope原型 */ Scope.prototype = { constructor: Scope, /** * 添加观察者,向$$watchers数组中添加一个对象 * @param watchFn * @param listenerFn */ $watch: function (watchFn, listenerFn) { var watch = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watch); }, /** * 脏值检测 */ $digest: function () { var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if(dirty && !(ttl--)) { throw '10 digest iterations reached'; } } while (dirty) }, $$digestOnce: function () {var dirty; this.$$watchers.forEach(function (watch) { var newValue = watch.watchFn(); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue); dirty = true; watch.last = newValue; } }) return dirty; } } /** * 例子 */ window.onload = function () { var $input = $('#input'); var $content = $('#content'); //Scope实例 var $scope = new Scope(); $scope.name = 'foo'; /** * 添加一个观察者(Angular内部在指令编译过程中完成) */ $scope.$watch(function () { return $scope.name; }, function (newValue, oldValue) { $input.val(newValue); $content.text(newValue); console.log(newValue) }) /** * 执行脏值检测,视图更新 * (Angular内部,当DOM ready之后,Angular启动,然后进行指令编译过程,指令编译完成之后执行$digest循环) */ $scope.$digest(); /** * 视图上更改input的值,更新数据并执行脏值检测 */ $input.on('input', listener); $input.on('change', listener); function listener(e) { $scope.name = $(e.target).val(); $scope.$digest(); } }