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 < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); if (ch == '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); if (ch == 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && number.charAt(number.length - 1) == 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && number.charAt(number.length - 1) == 'e') { this.throwError('Invalid exponent'); } else { break; } } this.index++; } number = 1 * number; this.tokens.push({ index: start, text: number, json: true, fn: function() { return number; } }); }
上面的程式碼就是檢查從當前index開始的整個數字,包括帶小數點的情況,檢查完畢之後跳出loop,當前index向前進一個,以待以後檢查後續字串,最後儲存到lex例項的token陣列中,這裡的fn屬性就是以後執行時用到的,這裡的return number是利用了JS的閉包特性,number其實就是檢查時外層的number變數值.以1+2為例,這時index應該停在+這裡,在lex的while loop中,+檢查會跳到最後一個else裡,這裡有一個物件比較關鍵,OPERATORS,它儲存著所有運算子所對應的動作,比如這裡的+,對應的動作是
'+':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 < statements.length; i++) { var statement = statements[i]; if (statement) { value = statement(self, locals); } } return value; }; } } }
程式碼以一個無限loop的while開始,語句分析的時候是有運算子優先順序的,預設的順序是,這裡以函式名為排序
filterChain < expression < assignment < ternary < logicalOR < logicalAND < equality < relational < additive < multiplicative < unary < primary
中文翻譯下就是這樣的
過濾函式<一般表示式<賦值語句<三元運算<邏輯or<邏輯and<比較運算<關係運算<加減法運算<乘法運算<一元運算,最後則預設取第一個token的fn屬性
這裡以1+2的token為例,這裡會用到parse的expect方法,expect會用到peek方法
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 < statements.length; i++) { var statement = statements[i]; if (statement) { value = statement(self, locals); } } return value; }
好了,說完了生成執行表示式,其實parse的任務已經完成了,現在只需要把這個作為parseprovider的返回值了.
等會再回到rootscope的$watch函式解析裡去,我們可以先測試下parse解析生成執行表示式的效果,這裡貼一個獨立的帶parse的例子,不依賴angularjs,感興趣的可以戳這裡
總結
今天先說到這裡了,下次有空接著分析rootscope後續的方法.
作者宣告
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。
相關推薦
AngularJS 原始碼分析2
上一篇地址 本文主要分析RootScopeProvider和ParseProvider RootScopeProvider簡介 今天這個rootscope可是angularjs裡面比較活躍的一個provider,大家可以理解為一個模型M或者VM,它主要負責與控制器或者指令進行資料互動. 今天使用的原始碼跟上次
Netty Pipeline原始碼分析(2)
原文連結:wangwei.one/posts/netty… 前面 ,我們分析了Netty Pipeline的初始化及節點新增與刪除邏輯。接下來,我們將來分析Pipeline的事件傳播機制。 Netty版本:4.1.30 inBound事件傳播 示例 我們通過下面這個例子來演示Ne
lucene原始碼分析(2)讀取過程例項
1.官方提供的程式碼demo Analyzer analyzer = new StandardAnalyzer(); // Store the index in memory: Directory directory = new RAMDirec
x264裡的2pass指的是什麼意思 x264原始碼分析2 encode
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
Shiro原始碼分析(2) - 會話管理器(SessionManager)
本文在於分析Shiro原始碼,對於新學習的朋友可以參考 [開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。 本文對Shiro中的SessionManager進行分析,SessionMan
《2.uboot和系統移植-第6部分-2.6.uboot原始碼分析2-啟動第二階段》
《2.uboot和系統移植-第6部分-2.6.uboot原始碼分析2-啟動第二階段》 第一部分、章節目錄 2.6.1.start_armboot函式簡介 2.6.2.start_armboot解析1 2.6.3.記憶體使用排布 2.6.4.start_armboot解析2 2.6.5.s
am335x 核心原始碼分析2 LCD移植
1、/arch/arm/mach-omap2/board-am335xevm.c/lcdc_init(){得到LCD硬體引數struct da8xx_lcdc_platform_data} -> am33xx_register_lcdc() -> omap_device_
MyBatis原始碼分析-2-基礎支援層-反射模組-TypeParameterResolver/ObjectFactory
TypeParameterResolver: TypeParameterResolver的功能是:當存在複雜的繼承關係以及泛型定義時, TypeParameterResolver 可以幫助我們解析欄位、方法引數或方法返回值的型別。TypeParameterResolver 是在Refelctor
muduo原始碼分析(2) --記憶體分配
寫在前面: 這個原始碼是分析libevent-2.0.20-stable, 並非最新版本的libevent,作者並沒有全看原始碼,在這裡會推薦以下參考的一些網站,也歡迎大家在不足的地方提出來進行討論。 什麼都沒包裝的記憶體管理 預設情況下,l
Minix3原始碼分析(2)——系統初始化
minix3的啟動牽扯到幾次控制權轉移,它們發生在mpx386.s中的組合語言例程和start.c及main.c中的C語言例程之間。 彙編程式碼需要做許多工作,包括建立一個 棧幀 以便為C編譯器編譯的程式碼提供適當的環境,複製處理器所使用的表格來定義儲存器段,建
Spring5原始碼分析系列(四)Spring5原始碼分析2
本文緊接上文Spring5原始碼分析1,講解基於XML的依賴注入,文章參考自Tom老師視訊,下一篇文章將介紹基於Annotation的依賴注入。 基於XML的依賴注入 1、依賴注入發生的時間 當SpringIOC容器完成了Bean定義資源的定位、載入和解析註冊以後,IO
JDK1.8ArrayList原始碼分析2
E get(int index) 因為ArrayList是採用陣列結構來儲存的,所以它的get方法非常簡單,先是判斷一下有沒有越界,之後就可以直接通過陣列下標來獲取元素了,所以get的時間複雜度是O(1)。 /** * Returns the
谷歌瀏覽器的原始碼分析 2
這麼大的工程,我從哪裡開始呢?我認為從介面開始,這樣才可以快速地深入研究。下面就可以先嚐試修改一個chrome的關於對話方塊,上一次看到它是英語的,那麼我就來把它改成中文的吧,這樣有目標了。從chrome的工程裡可以看到它是支援多種語言的,在Windows平臺上支援多語言的標準做法,就是寫多個語言的DL
3.24 vchain原始碼分析2
接下來是合約的第二部分,直接上程式碼,註釋都在程式碼中 // Contract to sell and distribute VEN tokens // 分發VEN 代幣 contract VENSale is Owned{ /// chart of stage t
x264裡的2pass指的是什麼意思 x264原始碼分析2 encode
A:x264裡的2pass指的是什麼意思?另外stat是什麼意思, 比如有個引數--stats <string> Filename for 2 pass stats [/"%s/"]/n", defaults->rc.psz_stat_out )
新人分享——hadoop原始碼分析2
這裡配置好了這個類,獲取outputFormat -之後就是會去讀取切片的元資料資訊,然後獲取reduce數量,之後會設定作業進度,然後獲取可執行的maptask 執行maptask 在這裡跑任務,並且監控 我們來看maptaskRunable的run方法 這裡設定了一堆屬性在裡面 這裡map.r
Fabric 1.0原始碼分析(2) blockfile(區塊檔案儲存)
Fabric 1.0原始碼筆記 之 blockfile(區塊檔案儲存) 1、blockfile概述 blockfile,即Fabric區塊鏈區塊檔案儲存,預設目錄/var/hyperledger/production/ledgersData/chains,含in
redis cluster叢集的原始碼分析(2)
本文的分析主要介紹叢集中的槽和叢集中命令的執行。 一、叢集中的槽 1、槽的基本結構資訊 redis叢集通過分片的方式來儲存資料庫中的鍵值對:叢集的整個資料庫被分為16384個槽, 資料庫中的每個鍵屬於這16384個槽的一個,每個節點可以處理0
Android應用程式啟動過程原始碼分析(2)
Step 9. ActivityStack.startActivityUncheckedLocked 這個函式定義在frameworks/base/services/java/com/android/server/am/ActivityStack.java檔案中: view plain pu
React原始碼分析2 — 元件和物件的建立(createClass,createElement)
1 元件的建立 React受大家歡迎的一個重要原因就是可以自定義元件。這樣的一方面可以複用開發好的元件,實現一處開發,處處呼叫,另外也能使用別人開發好的元件,提高封裝性。另一方面使得程式碼結構很清晰,元件間耦合減少,方便維護。ES5建立元件時,呼叫React.