-
Notifications
You must be signed in to change notification settings - Fork 10
Description
Scope
双向绑定之scope改变
如图所示,Scope原型链里有两个重要的方法:$watch、$digest,用于观测当scope变化时更新视图
$watch():在当前scope里向$$watchers添加需要观察的字段及回调函数$digest():检查当前scope下所有待检查字段是否更新,如果有更新则执行回调函数
下面示例代码:
function Scope(){
this.$$watchers = []; //存放通过$watch注册的字段及回调函数
}
Scope.prototype = {
$watch: function(watchExp, listener, objectEquality){
var watcher = {
exp: watchExp, //待观察字段表达式
fn: listener, //回调函数
last: initWatchVal //待观测字段原值
}
this.$$watchers.push(watcher);
return deregisterWatch(){//处于性能考虑,注册后返回一个销毁函数,方便必要的时候销毁
var index = self.$$watchers.indexOf(watcher);
self.$$watchers.splice(index, 1);
}
},
$digest: function(){
var self = this;
forEach(this.$$watchers, function(watcher){
var last = watcher.last, new = watcher.exp(self);
if(new != last){
watcher.fn(last, new, self);
watcher.last = new;
}
})
}
}
调用方式很简单:
var scope = new Scope();
scope.a = 1;
var watcher = scope.$watch(a, function(){
console.log('changed');
//scope.a = 2; - 回调函数修改了scope
})
scope.$digest();
watcher(); //我们在digest后可以销毁这个监听函数
基本逻辑还是蛮简单的,但是里面有很多细节需要处理。
- 如果回调函数也更改了scope中待观察的字段呢?这个时候需要在回调函数执行后,再次进行digest检查,直到对应的scope字段不在改变为止;
- 如果回调函数一直修改scope,那么将会陷入死循环中,我们需要避免这种情况出现,可以设置一个最大digest次数,如果超出就报错并终止
- 观察对象的数据的对比问题,当
$watch第三个参数objectEquality未定义或者传递为undefined或者false,则默认进行引用对比,如果设置为true时则需要进行深度值对比 - 还要注意NaN这类特殊数据无法进行比较的情况
除了$watch外,还有两个接口函数:$watchGroup,$watchCollection。
如上#3中提到,$digest对数据对比有两种:引用对比和深度值对比,对于数组或者对象,我们需要通过深度对比才能检测到数据的改变,引用对比是无法实现的(指向永远一样)。然而如果仅仅对数组或对象增加元素或者删除属性操作,而进行一遍深度对比,未免影响性能,因此增加了$watchCollection这个方法用于处理这种情况,但是$watchCollection并不对数据进行深度对比,所以想使用$watchCollection检测数组或者对象某个字段属性值变化的话,是没有任何用处的。
如果我们需要对scope中多个值进行检测且共用一个回调函数时,可以直接通过$watchGroup进行处理,需要注意的是,$watchGroup对检测的每个值进行引用对比。 那么$watch与$watchGroup有什么区别呢,看下面例子:
var scope = new Scope();
scope.a = 1;
scope.b = 2;
scope.$watch(['a', 'b'],function(){
console.log('$watch changed');
});
scope.a = 2;
scope.$digest();//无输出
scope.$watchGroup(['a', 'b'], function(){
console.log('$watchGroup changed');
});
scope.a = 2;
scope.$digest();//打印出$watchGroup changed
因为$watch检测的那个数组整体是一个变量,相当于var arr = ['a', 'b']; scope.$watch(arr, function(){}),因为默认是引用对比,所以不会触发回调,而$watchGroup则是针对数组中的个元素进行引用对比,这就是主要区别。
scope还有一些其他与dirtycheck相关的重要方法:$apply,$evalSync,$$postDiggest,如下图:
还是用简单的代码解释说明:
function Scope(){
this.$$asyncQueue = this.$$postDigestQueue = [];
}
Scope.prototype = {
$apply: function(exp){
try{
exp(this);
}finaly{
//触发digest
this.$digest();
}
},
$evalSync: function(expr){
this.$$asyncQueue.push(expr)
},
$$postDigest: function(fn){
this.$$postDigestQueue.push(fn);
},
$digest: function(){
var self = this;
//在执行dirtyCheck前立即执行evalSync的表达式,从而直接修改scope值,避免下次循环才能检测到scope变化
forEach(this.$$asyncQueue,function(exp){
exp(self);
})
//Digest正常逻辑
...
//digest dirtyCheck之后的回调
forEach(this.$$postDigestQueue,function(fn){
fn(self);
})
}
}
图中还有一个重要的方法没有提到$applyAsync,这个方法类似于$apply,区别在于此方法是延迟执行的,使用场景,如发送换一个http请求,返回数据后更改scope。
scope继承与消息通知
Angular在默认情况下scope是继承得来的(也可以通过参数阻止继承),这在angular中是一个很重要的概念,与继承相关的api可以参见下图:
先来看个伪代码,这里只列出$new的概述,而$on,$broadcast,$emit是通过标准的sub/pub实现的,这里不再写出来了:
Scope.prototype.$new = function(isolate, parent){
var child = new Scope();
if(parent){//指定字scope的父元素
child.prototype = parent;
}else{
if(isolate){//隔离继承
return child;
}else{
child.prototype = this;
}
}
return child;
}
实际上来讲,scope的继承及消息机制还是比较简单的,希望深入了解的话,可以去查看源码。
Module及依赖注入
前端模块化开发先后经历了从简单的单体,到AMD/CMD, 再到如今的ES6/ES2017的import机制的演变,而Angular本身的模块管理非常简单,就是一个简单的对象,Angular创建的所有module都会保存在私有变量modules中,以便需要的时候获取,下面的代码简单的描述了AngularJS创建module的逻辑。
(function(global){
var modules = {};
global.angular.module = function(name,requires,configFn){
if(!requires) return modules[name];//获取已存在的module
if(modules[name]) modules[name] = null;//覆盖原来定义的Module
var moduleInstance = {
name: name,
requires: requires,
...
}
modules[name] = moduleInstance;
return moduleInstance;
};
})(window)
Angular Module返回了一系列的方法如.value(),.constant(),.factory(),.service(),.provider(),用来注册创建module内部的数据;事实上调用这些方法时,module中并不直接创建相关数据,而是将创建这些数据的基本逻辑保存到一个队列中(module.__invokeQueue),待其他地方调用module时,通过依赖注入由var injector = createInjector(modulesToLoad),查询到所有依赖modules中__invokeQueue,根据参数依次调用相对应的方法生成最终数据并保存起来,以便需要单独时候使用,而这里所说的injector也是唯一能够获取module所创建数据的入口,因为angular的module中不会保存任何注册的数据。
在module中注册创建数据
前面提到module中返回的一系列用于创建数据的方法,看似糊涂实际上确实很简单的,下面以我们平时写简单的JS为例,进行简单的说明:
- 创建变量:
var name = 1;对应在angular中,我们是通过module.value('name', 1)注册; - 创建常量:
const name = 1;//ES6;对应在angular中,则是通过module.constant('name',1)注册; - 构造函数:
function fn(){};new fn();对应angular中,则通过module.service('fn',function(){})注册,而在调用时,angular内部会主动的对传入的构造函数,执行new运算; - 基本函数:
var fn = function(){}; fn();对应angular中,则是通过module.factory('fn', function(){})注册的,而在调用时,在angular内部会主动的调用执行传入的函数,而这个函数可以返回对象或其他任何类型的数据;
还是要再次说明一下:上面所述angular的这些方法(还有其他一些尚未提到的方法),并不会真正意义的创建这些数据,只有在调用module时,通过执行createInjector(modulesToLoad),在其内部执行注册的数据并创建保存,module本身不会保存任何数据。
上面也提到了,module的这些方法是放入到一个待执行的队列中,我们可以通过module.__invokeQueue获取到这些数据,下面展示了大体的代码:
(function(global){
var modules = {};
global.angular = function(name,requires,configFn){
if(!requires) return modules[name];//获取又存在的module
if(modules[name]) modules[name] = null;//覆盖原来定义的Module
var invokeQueue = [];
var invokeLater = function(provider, method, insertMethod, queue){
var queue = queue || invokeQueue;
return function(){
queue[insertMethod || 'push']([provider, method, arguments]);
return moduleInstance;
}
}
var moduleInstance = {
name: name,
requires: requires,
__invokeQueue: invokeQueue,
value: invokeLater('$provide', 'value'),
constant: invokeLater('$provide', 'constant', 'unshift'),
factory: invokeLaterAndSetModuleName('$provide', 'factory'),
service: invokeLaterAndSetModuleName('$provide', 'service')
}
modules[name] = moduleInstance;
return moduleInstance;
};
})(window)
说明:invokeLater和invokeLaterAndSetModuleName功能是一样的,唯一的区别就是invokeLaterAndSetModuleName在调用时会额外传递一个函数,将当前moule的name作为这个外部函数的一个属性传递过去,代码如下:
function invokeLaterAndSetModuleName(provider, method, queue) {
if (!queue) queue = invokeQueue;
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
queue.push([provider, method, arguments]);
return moduleInstance;
};
}
下图展示了angular创建数据类型的基本API:
上图中还有两个我们没有提到的方法:.provider和.decorator,简单的说可以认为其他的几个方法(.constant除外)是对.provider的封装,而.decorator正如其名,当我们通过.service,.factory或.provider注册了一个对象时,如果后期我们需要给对象增加更多的属性或方法,这个时候就用到了.decorator。
依赖注入
前面提到当我们加载module时,.createInjetor将会加载所需要的module,并执行保存这些module中的所有数据,以备需要的时候使用,下面展示了.createInjetor的基本逻辑:
function createInjector(modulesToLoad){
var cache = {}, self = this;
var value = function(){ ... };
var provider = function(){ ... };
var factory = function(){ ... };
var service = function(){ ... };
var constant = function(key, value){//仅以constant为例,简单说明
if(!cache[key]) cache[key] = value;
else return cache[key]
}
forEach(modulesToLoad, function loadModule(module){
if(module.requires) loadModule(module.requires);//加载深度依赖
forEach(module.__invokeQueue,function(invokeArgs){
var method = invokeArgs[1],args = invokeArgs[2];
method.apply(self, args);
})
})
return {
get:function(key){
return cache[key]
}
}
}
此时我们通过var injector = createInjector(['module'])加载相应的module时,便会在其内部生成所以来module的所有数据,当需要时通过injector.get来获取.
所以angular中所有的接口都是依赖于module的,当我们需要module中注册的某个数据是必然会通过createInjector自动加载对应的module并缓存数据被需要时使用。angular中主要通过以下几种方式,来告诉程序我们需要module中的数据,当然首先我们需要创建已给module,并注册一个名字为name的数据:var module = angular.module('module',[]);module.value('name', 1),这里的name将是我们下面程序需要的数据。
- var fn = function(){}; fn.$inject = ['name'];
- var fn = function(name){};
- var args = ['name', function(a){}],其中a就代表name
而createInjector会返回一个名为.annotate的方法来分析待执行函数所以来的数据名字(正则表达式等手段),如上面提到的name这个数据,然后通过.invoke调用待执行的函数。
function createInjector(modulesToLoad){
...
return {
invoke: invoke
get: get
annotate: annotate
}
}



