Skip to content

AngularJS核心概述 - Scope,Module,Injector #19

@abcrun

Description

@abcrun

Scope

双向绑定之scope改变

Scope:$wacth-$digest

如图所示,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后可以销毁这个监听函数

基本逻辑还是蛮简单的,但是里面有很多细节需要处理。

  1. 如果回调函数也更改了scope中待观察的字段呢?这个时候需要在回调函数执行后,再次进行digest检查,直到对应的scope字段不在改变为止;
  2. 如果回调函数一直修改scope,那么将会陷入死循环中,我们需要避免这种情况出现,可以设置一个最大digest次数,如果超出就报错并终止
  3. 观察对象的数据的对比问题,当$watch第三个参数objectEquality未定义或者传递为undefined或者false,则默认进行引用对比,如果设置为true时则需要进行深度值对比
  4. 还要注意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,如下图:

Scope:$apply-$evalAsync

还是用简单的代码解释说明:

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可以参见下图:

Scope:$apply-$evalAsync

先来看个伪代码,这里只列出$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)

说明invokeLaterinvokeLaterAndSetModuleName功能是一样的,唯一的区别就是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:

Module:Module-basicType

上图中还有两个我们没有提到的方法:.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将是我们下面程序需要的数据。

  1. var fn = function(){}; fn.$inject = ['name'];
  2. var fn = function(name){};
  3. var args = ['name', function(a){}],其中a就代表name

createInjector会返回一个名为.annotate的方法来分析待执行函数所以来的数据名字(正则表达式等手段),如上面提到的name这个数据,然后通过.invoke调用待执行的函数。

function createInjector(modulesToLoad){
	...
	return {
		invoke: invoke
		get: get
		annotate: annotate
	}
}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions