angular 1.0源代码分析以及与vue和react比较
本文分析angular 1.0从初始化开始到编译网页更新页面的源代码过程以及一些重要细节。
测试项目例子:
<html ng-app='myapp' >
<body ng-controller="myController" >
<tip title="title"></tip>
</body>
controller和指令用angular.module的方法定义。
angular 1.0 以指令为中心,directive指令标签就是组件,有template,而属性指令主要是修改元素属性,angular 2.0改为以组件为中心设计。
angular入口初始化程序:
function angularInit(element, bootstrap) {
bootstrap(appElement, module ? [module] : []);
}
function bootstrap(element, modules) {
var doBootstrap = function() {
modules = modules || [];
modules.unshift(['$provide', function($provide) {
$provide.value('$rootElement', element);
}]);
modules.unshift('ng');
var injector = createInjector(modules);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', //angular代码执行时已经创建内部对象,这些是对象的key(名字)
function(scope, element, compile, injector, animate) { // invoke([模块1,模块2,...,fn])就是调用执行fn,传递依赖模块(angular内部对象)
scope.$apply(function() { // 外套$apply执行方法,执行完方法之后扫描watcher重新获取所有watcher表达式的值进行必要的页面更新
element.data('$injector', injector);
compile(element)(scope); // 编译根元素,返回link函数,再执行link函数,更新页面的代码在每个指令表达式watcher的update方法中。
});
}]);
return injector;
};
return doBootstrap();
}
function createInternalInjector(cache, factory) {
function invoke(fn, self, locals){ //invoke就是变换参数和作用域调用函数, angular内部对象机制和依赖模块注入非常复杂,本文忽略
fn = fn[length];
return fn.apply(self, args);
下面来看compile()代码:
function compile($compileNodes, transcludeFn, maxPrior //从根元素开始编译
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, // 递归编译子节点
function compileNodes(nodeList, transcludeFn, $rootElement,
applyDirectivesToNode(directives,
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
$compileNode.html(directiveValue); // 把指令的template<div>{{title}}</div>插入网页中的节点元素中,
childLinkFn = compileNodes(childNodes,nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); // 有子节点则递归调用自身 return linkFnFound ? compositeLinkFn : null//每一次递归子节点时已经编译了子节点的指令,递归子节点层层返回到最上层compile代码位置时,返回的link函数已经包含每一层递归时产生的link函数,也就是每一层递归时编译结果。最后只要再执行最终返回的link函数,传递根scope,把根scope保存在根元素对象属性中,就完成了整个编译插入网页的过程,在每一次递归编译子节点时如果有指令会编译指令更新子节点生效。路由
组件有template是编译插入网页生效。
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { //每一次递归返回的通用link函数,含每一次递归编译的结果数据
//递归编译子节点每次都要产生这个link函数,编译完成执行link函数时,也是层层递归每一个子节点执行子节点的link函数(含编译结果数据),执行link函数的过程跟执行compile函数的过程一样复杂,每个子节点都要递归处理一遍,包括网页中的换行文本text节点。
angular要扫描编译整个网页,循环递归处理所有的元素节点,没有必要,应该只编译template插入到占位元素。
if (nodeLinkFn) {
if (nodeLinkFn.scope) {
childScope = scope.$new();
$node.data('$scope', childScope);
} else {
childScope = scope;
}
function nodeLinkFn(childLinkFn, scope, linkNod // 编译每个节点产生的通用link函数
controllerInstance = $controller(controller, locals); // body元素有指令调用controller
return function(expression, locals) {
instance = $injector.instantiate(expression, locals); // exp就是controller构造函数,这是生成一个实例继承controller构造函数,不是new controller实例,
$controller()是生成一个实例继承controller构造函数
function instantiate(Type, locals) {
var Constructor = function() {},
instance, returnedValue;
//定义模块还有一种写法是 someModule.factory('greeter', ['$window', function(renamed$window) {}]); []中前面是依赖模块,最后一个是构造函数
Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype;
instance = new Constructor();
returnedValue = invoke(Type, instance, locals); //调用执行controller构造函数
return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
}
function invoke(fn, self, locals){
//准备依赖对象,args依赖对象用locals[key]或getService(key)方法获取
return fn.apply(self, args); //调用执行controller构造函数,是在这里执行controller初始化代码的
//因此controller代码被执行时this是一个instance但不是new controller实例
//至此可以知道在link阶段执行controller初始化代码,设置title属性=hello,更新之后网页中的{{title}}被hello替换
$element.data('$' + directive.name + 'Controller', controllerInstance); // $ng-controllerController
//可见scope和controller instance会保存在元素对象属性中,在需要的时候可以从元素对象中获取scope或controller instance
//递归结束之后返回到这里执行时网页显示{{title}}
return function publicLinkFn(scope, cloneConnectFn, // compile返回的通用link函数,返回后再执行link函数传递scope
$linkNode.eq(i).data('$scope', scope); // node是元素jquery对象,把scope保存到元素对象的属性中
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
return $linkNode;
小结一下:
compile编译是解析处理页面表达式,比如{{title}},比如ng-controller="mycontroller"。
而link函数是处理数据层面,执行link函数需要传递 scope,因为数据作用域是scope,执行controller代码初始化数据是在link函数进行的,是用fn.apply方式执行controller构造函数,compile和link函数执行完之后,数据层面处理已经结束,比如controller的scope.title="hello"已经初始化,但页面没有变化,需要更新页面,把数据插入页面,这是通过$apply->$digest->watcher->fn.apply完成的。
编译方法compile外套$apply,$apply会调用$digest:
$digest: function() {
//递归scope找watcher,watcher数据中没有scope,因此执行watcher的方法时要传递scope,执行watcher的方法时会变换作用域为scope
value = watch.get(current) // 获取表达式的值时要传递scope,表达式{{title}}的值是hello
watch.fn(value, ((last === initWatchVal) ? value : last), current); // 执行watch.fn之后网页显示hello,fn就是handler/update函数
代码中还涉及到defer和settimeout是延迟函数,实现异步调度,非功能性流程。
因此angular在初始化时编译网页时针对每个表达式建立了watcher,编译程序外套$apply,会调用$digest扫描执行watcher的update方法更新网页。
vue也是在初始化编译template时针对每个表达式建立watcher,为组件的data属性建立set/get方法,只要set数据操作,就会执行相应的watcher的update更新页面。
vue 2.0是针对组件建立watcher,初始化时编译根组件执行根组件watcher的update更新页面,set数据时执行相应的组件的watcher的update方法更新页面,组件
可能有子组件嵌套,那么从组件递归重新编译template产生vnode,再根据vnode更新页面。
angular是在数据操作之后执行$digest扫描执行watcher更新页面,vue是set触发执行watcher更新页面,angular和vue都使用watcher机制来实现组件页面更新问题,
watcher中含表达式和更新方法,当数据变化触发更新时,只需要执行相应的watcher的update更新方法更新页面。
react是在初始化编译时递归编译子节点,然后把编译结果插入网页生效,当数据变化时,通过setState执行runBatchedUpdates -> performUpdateIfNecessary -> updateComponent,重新编译dirty component组件更新页面,react不用watcher机制。
angular和vue都是用compile方法递归编译网页元素,这是它们的核心程序,react也是类似的,其mountComponent其实就是递归编译程序,都是设计一个核心编译程序,递归编译所有的子节点。
在template和表达式的写法方面不太一样,react比较特殊,它用render()方法写template,用JSX语法,用babel编译解析,产生一个含层层嵌套的createElement()的函数,执行这个函数就产生一个根元素对象,里面含层层嵌套的子元素对象,再编译这个根元素对象,层层递归子元素对象。
vue 2.0也是用类似的方法,编译template产生一个render方法代码,含层层嵌套的createElement方法,再执行render方法代码产生一个根元素Element对象,里面含层层嵌套的子元素对象,再编译这个根元素对象,层层递归子元素对象,这是前端框架编译与网页tree对应的元素对象的唯一设计方法,也是最好的办法,没有其它办法或更好的办法。
总体来说,angular和react是高大上的软件,源代码非常复杂如天书一般,非常人能看懂源代码,而vue是个人设计的大众化软件,相对简单实用,有些设计也很巧妙很聪明,比如数据响应设计方法(get/set方法)个人认为超越了angular和react的设计,应该可以成为浏览器和框架的新标准,因为它太好了,很简单就把难题给解决了,何必像react使用redux那么复杂费劲地折腾呢。vue还有一个好的地方就是它使用js标准对象方法,就是new fn()方法,非常好理解,非常容易看和跟踪源代码,而angular使用内部模块机制,一般人就看不懂了,其实用webpack开发再加上框架的组件机制,已经充分实现了模块化编程,框架自己再搞一套模块机制已经没有意义了,angular 1.0也早已经被google放弃了。