AngularJS 源码分析2
本文主要分析RootScopeProvider和ParseProvider
RootScopeProvider简介
今天这个rootscope可是angularjs里面比较活跃的一个provider,大家可以理解为一个模型M或者VM,它主要负责与控制器或者指令进行数据交互.
今天使用的源码跟上次分析的一样也是1.2.X系列,只不过这次用的是未压缩合并版的,方便大家阅读,可以在这里下载
从$get属性说起
说起这个$get属性,是每个系统provider都有的,主要是先保存要实例化的函数体,等待instanceinjector.invoke的时候来调用,因为$get的代码比较多,所以先上要讲的那部分,大家可以注意到了,在$get上面有一个digestTtl方法
this.digestTtl = function(value) {
if (arguments.length) {
TTL = value;
}
return TTL;
};
这个是用来修改系统默认的dirty check次数的,默认是10次,通过在config里引用rootscopeprovider,可以调用这个方法传递不同的值来修改ttl(short for Time To Live)
下面来看下$get中的scope构造函数
function Scope() {
this.$id = nextUid();
this.$$phase = this.$parent = this.$$watchers =
this.$$nextSibling = this.$$prevSibling =
this.$$childHead = this.$$childTail = null;
this['this'] = this.$root = this;
this.$$destroyed = false;
this.$$asyncQueue = [];
this.$$postDigestQueue = [];
this.$$listeners = {};
this.$$listenerCount = {};
this.$$isolateBindings = {};
}
可以看到在构造函数里定义了很多属性,我们来一一说明一下
- $id, 通过nextUid方法来生成一个唯一的标识
- $$phase, 这是一个状态标识,一般在dirty check时用到,表明现在在哪个阶段
- $parent, 代表自己的上级scope属性
- $$watchers, 保存scope变量当前所有的监控数据,是一个数组
- $$nextSibling, 下一个兄弟scope属性
- $$prevSibling, 前一个兄弟scope属性
- $$childHead, 第一个子级scope属性
- $$childTail, 最后一个子级scope属性
- $$destroyed, 表示是否被销毁
- $$asyncQueue, 代表异步操作的数组
- $$postDigestQueue, 代表一个在dirty check之后执行的数组
- $$listeners, 代表scope变量当前所有的监听数据,是一个数组
- $$listenerCount, 暂无
- $$isolateBindings, 暂无
通过这段代码,可以看出,系统默认会创建根作用域,并作为$rootScopeprovider实例返回.
var $rootScope = new Scope(); return $rootScope;
创建子级作用域是通过$new方法,我们来看看.
$new: function(isolate) {
var ChildScope,
child;if (isolate) {
child = new Scope();
child.$root = this.$root;
// ensure that there is just one async queue per $rootScope and its children
child.$$asyncQueue = this.$$asyncQueue;
child.$$postDigestQueue = this.$$postDigestQueue;
} else {
// Only create a child scope class if somebody asks for one,
// but cache it to allow the VM to optimize lookups.
if (!this.$$childScopeClass) {
this.$$childScopeClass = function() {
this.$$watchers = this.$$nextSibling =
this.$$childHead = this.$$childTail = null;
this.$$listeners = {};
this.$$listenerCount = {};
this.$id = nextUid();
this.$$childScopeClass = null;
};
this.$$childScopeClass.prototype = this;
}
child = new this.$$childScopeClass();
}
child['this'] = child;
child.$parent = this;
child.$$prevSibling = this.$$childTail;
if (this.$$childHead) {
this.$$childTail.$$nextSibling = child;
this.$$childTail = child;
} else {
this.$$childHead = this.$$childTail = child;
}
return child;
}
通过分析上面的代码,可以得出
isolate标识来创建独立作用域,这个在创建指令,并且scope属性定义的情况下,会触发这种情况,还有几种别的特殊情况,假如是独立作用域的话,会多一个$root属性,这个默认是指向rootscope的
如果不是独立的作用域,则会生成一个内部的构造函数,把此构造函数的prototype指向当前scope实例
通用的操作就是,设置当前作用域的$$childTail,$$childTail.$$nextSibling,$$childHead,this.$$childTail为生成的子级作用域;设置子级域的$parent为当前作用域,$$prevSibling为当前作用域最后一个子级作用域
说完了创建作用域,再来说说$watch函数,这个比较关键
$watch: function(watchExp, listener, objectEquality) {
var scope = this,
get = compileToFn(watchExp, 'watch'),
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: watchExp,
eq: !!objectEquality
};lastDirtyWatch = null; // in the case user pass string, we need to compile it, do we really need this ?
if (!isFunction(listener)) {
var listenFn = compileToFn(listener || noop, 'listener');
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
} if (typeof watchExp == 'string' && get.constant) {
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
} if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher); return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
$watch函数有三个参数,第一个是监控参数,可以是字符串或者函数,第二个是监听函数,第三个是代表是否深度监听,注意看这个代码
get = compileToFn(watchExp, 'watch')
这个compileToFn函数其实是调用$parse实例来分析监控参数,然后返回一个函数,这个会在dirty check里用到,用来获取监控表达式的值,这个$parseprovider也是angularjs中用的比较多的,下面来重点的说下这个provider
$parse的代码比较长,在源码文件夹中的ng目录里,parse.js里就是$parse的全部代码,当你了解完parse的核心之后,这部份代码其实可以独立出来,做成自己的计算器程序也是可以的,因为它的核心就是解析字符串,而且默认支持四则运算,运算符号的优先级处理,只是额外的增加了对变量的支持以及过滤器的支持,想想,把这块代码放在模板引擎里也是可以的,说多了,让我们来一步一步的分析parse代码吧.
记住,不管是哪个provider,先看它的$get属性,所以我们先来看看$parse的$get吧
this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) {
$parseOptions.csp = $sniffer.csp;promiseWarning = function promiseWarningFn(fullExp) {
if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return;
promiseWarningCache[fullExp] = true;
$log.warn('[$parse] Promise found in the expression ' + fullExp + '. ' +
'Automatic unwrapping of promises in Angular expressions is deprecated.');
}; return function(exp) {
var parsedExpression; switch (typeof exp) {
case 'string': if (cache.hasOwnProperty(exp)) {
return cache[exp];
} var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp, false); if (exp !== 'hasOwnProperty') {
// Only cache the value if it's not going to mess up the cache object
// This is more performant that using Object.prototype.hasOwnProperty.call
cache[exp] = parsedExpression;
} return parsedExpression; case 'function':
return exp; default:
return noop;
}
};
}];
可以看出,假如解析的是函数,则直接返回,是字符串的话,则需要进行parser.parse方法,这里重点说下这个
通过阅读parse.js文件,你会发现,这里有两个关键类
lexer, 负责解析字符串,然后生成token,有点类似编译原理中的词法分析器
parser, 负责对lexer生成的token,生成执行表达式,其实就是返回一个执行函数
看这里
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp, false);
第一句就是创建一个lexer实例,第二句是把lexer实例传给parser构造函数,然后生成parser实例,最后一句是调用parser.parse生成执行表达式,实质是一个函数
现在转到parser.parse里去
parse: function (text, json) {
this.text = text;//TODO(i): strip all the obsolte json stuff from this file
this.json = json; this.tokens = this.lexer.lex(text); console.log(this.tokens); if (json) {
// The extra level of aliasing is here, just in case the lexer misses something, so that
// we prevent any accidental execution in JSON.
this.assignment = this.logicalOR; this.functionCall =
this.fieldAccess =
this.objectIndex =
this.filterChain = function() {
this.throwError('is not valid json', {text: text, index: 0});
};
} var value = json ? this.primary() : this.statements(); if (this.tokens.length !== 0) {
this.throwError('is an unexpected token', this.tokens[0]);
} value.literal = !!value.literal;
value.constant = !!value.constant; return value;
}
视线移到这句this.tokens = this.lexer.lex(text),然后来看看lex方法
lex: function (text) {
this.text = text;this.index = 0;
this.ch = undefined;
this.lastCh = ':'; // can start regexp this.tokens = []; var token;
var json = []; while (this.index < this.text.length) {
this.ch = this.text.charAt(this.index);
if (this.is('"\'')) {
this.readString(this.ch);
} else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdent(this.ch)) {
this.readIdent();
// identifiers can only be if the preceding char was a { or ,
if (this.was('{,') && json[0] === '{' &&
(token = this.tokens[this.tokens.length - 1])) {
token.json = token.text.indexOf('.') === -1;
}
} else if (this.is('(){}[].,;:?')) {
this.tokens.push({
index: this.index,
text: this.ch,
json: (this.was(':[,') && this.is('{[')) || this.is('}]:,')
});
if (this.is('{[')) json.unshift(this.ch);
if (this.is('}]')) json.shift();
this.index++;
} else if (this.isWhitespace(this.ch)) {
this.index++;
continue;
} else {
var ch2 = this.ch + this.peek();
var ch3 = ch2 + this.peek(2);
var fn = OPERATORS[this.ch];
var fn2 = OPERATORS[ch2];
var fn3 = OPERATORS[ch3];
if (fn3) {
this.tokens.push({index: this.index, text: ch3, fn: fn3});
this.index += 3;
} else if (fn2) {
this.tokens.push({index: this.index, text: ch2, fn: fn2});
this.index += 2;
} else if (fn) {
this.tokens.push({
index: this.index,
text: this.ch,
fn: fn,
json: (this.was('[,:') && this.is('+-'))
});
this.index += 1;
} else {
this.throwError('Unexpected next character ', this.index, this.index + 1);
}
}
this.lastCh = this.ch;
}
return this.tokens;
}
这里我们假如传进的字符串是1+2,通常我们分析源码的时候,碰到代码复杂的地方,我们可以简单化处理,因为逻辑都一样,只是情况不一样罢了.
上面的代码主要就是分析传入到lex内的字符串,以一个whileloop开始,然后依次检查当前字符是否是数字,是否是变量标识等,假如是数字的话,则转到
readNumber方法,这里以1+2为例,当前ch是1,然后跳到readNumber方法
readNumber: function() {
var number = '';
var start = this.index;
while (this.index
'+':function(self, locals, a,b){
a=a(self, locals); b=b(self, locals);
if (isDefined(a)) {
if (isDefined(b)) {
return a + b;
}
return a;
}
return isDefined(b)?b:undefined;}
大家注意了,这里有4个参数,可以先透露一下,第一个是传的是当前上下文对象,比喻当前scope实例,这个是为了获取字符串中的变量值,第二个参数是本地变量,是传递给函数当入参用的,基本用不到,最后两个参是关键,+是二元运算符,所以a代表左侧运算值,b代表右侧运算值.
最后解析完+之后,index停在了2的位置,跟1一样,也是返回一个token,fn属性也是一个返回当前数字的函数.
当解析完整个1+2字符串后,lex返回的是token数组,这个即可传递给parse来处理,来看看
var value = json ? this.primary() : this.statements();
默认json是false,所以会跳到this.statements(),这里将会生成执行语句.
statements: function() {
var statements = [];
while (true) {
if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
statements.push(this.filterChain());
if (!this.expect(';')) {
// optimize for the common case where there is only one statement.
// TODO(size): maybe we should not support multiple statements?
return (statements.length === 1)
? statements[0]
: function(self, locals) {
var value;
for (var i = 0; i
peek: function(e1, e2, e3, e4) {
if (this.tokens.length > 0) {
var token = this.tokens[0];
var t = token.text;
if (t === e1 || t === e2 || t === e3 || t === e4 ||
(!e1 && !e2 && !e3 && !e4)) {
return token;
}
}
return false;
}, expect: function(e1, e2, e3, e4){
var token = this.peek(e1, e2, e3, e4);
if (token) {
if (this.json && !token.json) {
this.throwError('is not valid json', token);
}
this.tokens.shift();
return token;
}
return false;
}
expect方法传空就是默认从token数组中弹出第一个token,数组数量减1
1+2的执行语句最后会定位到加法运算那里additive
additive: function() {
var left = this.multiplicative();
var token;
while ((token = this.expect('+','-'))) {
left = this.binaryFn(left, token.fn, this.multiplicative());
}
return left;
}
最后返回一个二元操作的函数binaryFn
binaryFn: function(left, fn, right) {
return extend(function(self, locals) {
return fn(self, locals, left, right);
}, {
constant:left.constant && right.constant
});
}
这个函数参数里的left,right对应的'1','2'两个token的fn属性,即是
js
function(){ return number;}
fn函数对应additive方法中+号对应token的fn
function(self, locals, a,b){
a=a(self, locals); b=b(self, locals);
if (isDefined(a)) {
if (isDefined(b)) {
return a + b;
}
return a;
}
return isDefined(b)?b:undefined;}
最后生成执行表达式函数,也就是filterChain返回的left值,被push到statements方法中的statements数组中,仔细看statements方法的返回值,假如表达式数组长度为1,则返回第一个执行表达式,否则返回一个包装的函数,里面是一个loop,不断的执行表达式,只返回最后一个表达式的值
return (statements.length === 1)
? statements[0]
: function(self, locals) {
var value;
for (var i = 0; i
好了,说完了生成执行表达式,其实parse的任务已经完成了,现在只需要把这个作为parseprovider的返回值了.
等会再回到rootscope的$watch函数解析里去,我们可以先测试下parse解析生成执行表达式的效果,这里贴一个独立的带parse的例子,不依赖angularjs,感兴趣的可以戳这里
总结
今天先说到这里了,下次有空接着分析rootscope后续的方法.
作者声明
作者: feenan
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
AngularJS 源码分析2的更多相关文章
- angularjs源码分析之:angularjs执行流程
angularjs用了快一个月了,最难的不是代码本身,而是学会怎么用angular的思路思考问题.其中涉及到很多概念,比如:directive,controller,service,compile,l ...
- AngularJS 源码分析1
AngularJS简介 angularjs 是google出品的一款MVVM前端框架,包含一个精简的类jquery库,创新的开发了以指令的方式来组件化前端开发,可以去它的官网看看,请戳这里 再贴上一个 ...
- AngularJS 源码分析3
本文接着上一篇讲 上一篇地址 回顾 上次说到了rootScope里的$watch方法中的解析监控表达式,即而引出了对parse的分析,今天我们接着这里继续挖代码. $watch续 先上一块$watch ...
- AngularJS源码分析之依赖注入$injector
开篇 随着javaEE的spring框架的兴起,依赖注入(IoC)的概念彻底深入人心,它彻底改变了我们的编码模式和思维.在IoC之前,我们在程序中需要创建一个对象很简单也很直接,就是在代码中new O ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- MVVM大比拼之AngularJS源码精析
MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...
- ABP源码分析三十七:ABP.Web.Api Script Proxy API
ABP提供Script Proxy WebApi为所有的Dynamic WebApi生成访问这些WebApi的JQuery代理,AngularJs代理以及TypeScriptor代理.这些个代理就是j ...
- angular源码分析:angular中入境检察官$sce
一.ng-bing-html指令问题 需求:我需要将一个变量$scope.x = '<a href="http://www.cnblogs.com/web2-developer/&qu ...
- angular源码分析:angular中脏活累活承担者之$parse
我们在上一期中讲 $rootscope时,看到$rootscope是依赖$prase,其实不止是$rootscope,翻看angular的源码随便翻翻就可以发现很多地方是依赖于$parse的.而$pa ...
随机推荐
- Servlet读取Excel标准化数据库过程记录
完成数据库的连接 获取连接参数 拷贝1.数据库URL 2.驱动程序类 3.用户 编写Servlet 1.创建连接对象 Connection con = null; PreparedStatement ...
- 在HyperlinkButton的URL地址里附加多个参数(以http get的方式)
1.使用 Uri(string uriString,UriType type)创建Uri对象:new Uri("/navigatingPage?key1=value1&key2=va ...
- 搜索技巧<转>
平时工作,搜索引擎是少不了的,作为程序员,当然首推 Google.这里简单介绍下几个 Google 搜索的小技巧,方便别人也方便自己查阅. ps:以下所有操作,均可以在 「谷歌搜索首页 -> 设 ...
- 螺旋方阵(4x4)(java实现)
代码如下: public class N { public static void main(String[] args) { final int N=4; int a[][]=new int[N][ ...
- css自定义三角形效果
废话不说了,直接上代码 element{ width:0px; height:0px; border-left:10px; border-right:10px; border-bottom:10px; ...
- 配置nginx的图片服务器
user nginx; worker_processes 8; error_log /usr/local/webserver/nginx/logs/nginx_error.log crit; pid ...
- 基于shell脚本比较数字大小
让用户输入两个数来比较他们的大小 先用touch命令新建一个1.sh文件 在用vi进入i进入编辑状态 输入 #!/bin/bash read "" a read "&qu ...
- nginx的日常应用
nginx的配置文件nginx.conf配置详解如下: user nginx nginx ; Nginx用户及组:用户 组.window下不指定 worker_processes ; 工作进程:数目. ...
- linux工具
sudo yum install yum-utils
- Javascript的自执行函数
自执行函数其实也就是"立即执行的函数",它有四个特点:提高性能.利于压缩.避免冲突.依赖加载: 1.减少作用域查找 JS代码: // Anonymous function that ...