1. 程式人生 > >AngularJS 原始碼分析2

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.