精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

構建自己的AngularJS(1):Scope和Digest

開發 前端
Angular是一個成熟和強大的JavaScript框架。它也是一個比較龐大的框架,在熟練掌握之前,需要領會它提出的很多新概念。很多Web 開發人員涌向Angular,有不少人面臨同樣的障礙。Digest到底是怎么做的?定義一個指令(directive)有哪些不同的方 法?Service和provider有什么區別?

Angular是一個成熟和強大的JavaScript框架。它也是一個比較龐大的框架,在熟練掌握之前,需要領會它提出的很多新概念。很多Web 開發人員涌向Angular,有不少人面臨同樣的障礙。Digest到底是怎么做的?定義一個指令(directive)有哪些不同的方 法?Service和provider有什么區別?

Angular的文檔挺不錯的,第三方的資源也越來越豐富,想要學習一門新的技術,沒什么方法比把它拆開研究其運作機制更好。

在這個系列的文章中,我將從無到有構建AngularJS的一個實現。隨著逐步深入的講解,讀者將能對Angular的運作機制有一個深入的認識。

在***部分中,讀者將看到Angular的作用域是如何運作的,還有比如$eval, $digest, $apply這些東西怎么實現。Angular的臟檢查邏輯看上去有些不可思議,但你將看到實際并非如此。

基礎知識

在Github上,可以看到這個項目的全部源碼。相比只復制一份下來,我更建議讀者從無到有構建自己的實現,從不同角度探索代碼的每個步驟。在本文 中,我嵌入了JSBin的一些代碼,可以直接在文章中進行一些互動。(譯者注:因為我在github上翻譯,沒法集成JSBin了,只能給鏈接……)

我們將使用Lo-Dash庫來處理一些在數組和對象上的底層操作。Angular自身并未使用Lo-Dash,但是從我們的目的看,要盡量無視這些不太相關的比較底層的事情。當讀者在代碼中看到下劃線(_)的時候,那就是在調用Lo-Dash的功能。

我們還將使用console.assert函數做一些特別的測試。這個函數應該適用于所有現代JavaScript環境。

下面是使用Lo-Dash和assert函數的示例:

http://jsbin.com/UGOVUk/4/embed?js,console

Scope對象

Angular的Scope對象是POJO(簡單的JavaScript對象),在它們上面,可以像對其他對象一樣添加屬性。Scope對象是用構造函數創建的,我們來寫個最簡單的版本:

  1. function Scope() { 

現在我們就可以使用new操作符來創建一個Scope對象了。我們也可以在它上面附加一些屬性:

  1. var aScope = new Scope(); 
  2. aScope.firstName = 'Jane'
  3. aScope.lastName = 'Smith'

這些屬性沒什么特別的。不需要調用特別的設置器(setter),賦值的時候也沒什么限制。相反,在兩個特別的函數:$watch和$digest之中發生了一些奇妙的事情。

監控對象屬性:$watch和$digest

$watch和$digest是相輔相成的。兩者一起,構成了Angular作用域的核心:數據變化的響應。

使用$watch,可以在Scope上添加一個監聽器。當Scope上發生變更時,監聽器會收到提示。給$watch指定如下兩個函數,就可以創建一個監聽器:

  • 一個監控函數,用于指定所關注的那部分數據。
  • 一個監聽函數,用于在數據變更的時候接受提示。

作為一名Angular用戶,一般來說,是監控一個表達式,而不是使用監控函數。監控表達式是一個字符串,比如說 “user.firstName”,通常在數據綁定,指令的屬性,或者JavaScript代碼中指定,它被Angular解析和編譯成一個監控函數。在 這篇文章的后面部分我們會探討這是如何做的。在這篇文章中,我們將使用稍微低級的方法直接提供監控功能。

為了實現$watch,我們需要存儲注冊過的所有監聽器。我們在Scope構造函數上添加一個數組:

  1. function Scope() { 
  2.   this.$$watchers = []; 

在Angular框架中,雙美元符前綴$$表示這個變量被當作私有的來考慮,不應當在外部代碼中調用。

現在我們可以定義$watch方法了。它接受兩個函數作參數,把它們存儲在$$watchers數組中。我們需要在每個Scope實例上存儲這些函數,所以要把它放在Scope的原型上:

  1. Scope.prototype.$watch = function(watchFn, listenerFn) { 
  2.   var watcher = { 
  3.     watchFn: watchFn, 
  4.     listenerFn: listenerFn 
  5.   }; 
  6.   this.$$watchers.push(watcher); 
  7. }; 

另外一面就是$digest函數。它執行了所有在作用域上注冊過的監聽器。我們來實現一個它的簡化版,遍歷所有監聽器,調用它們的監聽函數:

  1. Scope.prototype.$digest = function() { 
  2.   _.forEach(this.$$watchers, function(watch) { 
  3.     watch.listenerFn(); 
  4.   });  
  5. }; 

現在我們可以添加監聽器,然后運行$digest了,這將會調用監聽函數:

http://jsbin.com/oMaQoxa/2/embed?js,console

這些本身沒什么大用,我們要的是能檢測由監控函數指定的值是否確實變更了,然后調用監聽函數。

臟值檢測

如同上文所述,監聽器的監聽函數應當返回我們所關注的那部分數據的變化,通常,這部分數據就存在于作用域中。為了使得訪問作用域更便利,在調用監控函數的時候,使用當前作用域作為實參。一個關注作用域上fiestName屬性的監聽器像這個樣子:

  1. function(scope) { 
  2.   return scope.firstName; 

#p#

這是監控函數的一般形式:從作用域獲取一些值,然后返回。

$digest函數的作用是調用這個監控函數,并且比較它返回的值和上一次返回值的差異。如果不相同,監聽器就是臟的,它的監聽函數就應當被調用。

想要這么做,$digest需要記住每個監控函數上次返回的值。既然我們現在已經為每個監聽器創建過一個對象,只要把上一次的值存在這上面就行了。下面是檢測每個監控函數值變更的$digest新實現:

  1. Scope.prototype.$digest = function() { 
  2.   var self = this
  3.   _.forEach(this.$$watchers, function(watch) { 
  4.     var newValue = watch.watchFn(self); 
  5.     var oldValue = watch.last; 
  6.     if (newValue !== oldValue) { 
  7.       watch.listenerFn(newValue, oldValue, self); 
  8.     } 
  9.     watch.last = newValue
  10.   });  
  11. }; 

對每個監聽器,我們調用監控函數,把作用域自身當作實參傳遞進去,然后比較這個返回值和上次返回值,如果不同,就調用監聽函數。方便起見,我們把新舊值和作用域都當作參數傳遞給監聽函數。最終,我們把監聽器的last屬性設置成新返回的值,下一次可以用它來作比較。

有了這個實現之后,我們就可以看到在$digest調用的時候,監聽函數是怎么執行的:

http://jsbin.com/OsITIZu/3/embed?js,console

我們已經實現了Angular作用域的本質:添加監聽器,在digest里運行它們。

也已經可以看到幾個關于Angular作用域的重要性能特性:

  • 在作用域上添加數據本身并不會有性能折扣。如果沒有監聽器在監控某個屬性,它在不在作用域上都無所謂。Angular并不會遍歷作用域的屬性,它遍歷的是監聽器。
  • $digest里會調用每個監控函數,因此,***關注監聽器的數量,還有每個獨立的監控函數或者表達式的性能。

在Digest的時候獲得提示

如果你想在每次Angular的作用域被digest的時候得到通知,可以利用每次digest的時候挨個執行監聽器這個事情,只要注冊一個沒有監聽函數的監聽器就可以了。

想要支持這個用例,我們需要在$watch里面檢測是否監控函數被省略了,如果是這樣,用個空函數來代替它:

  1. Scope.prototype.$watch = function(watchFn, listenerFn) { 
  2.   var watcher = { 
  3.     watchFn: watchFn, 
  4.     listenerFn: listenerFn || function() { } 
  5.   }; 
  6.   this.$$watchers.push(watcher); 
  7. }; 

如果用了這個模式,需要記住,即使沒有listenerFn,Angular也會尋找watchFn的返回值。如果返回了一個值,這個值會提交給臟檢查。想要采用這個用法又想避免多余的事情,只要監控函數不返回任何值就行了。在這個例子里,監聽器的值始終會是未定義的。

http://jsbin.com/OsITIZu/4/embed?js,console

這個實現的核心就這樣,但是離最終的還是差太遠了。比如說有個很典型的場景我們不能支持:監聽函數自身也修改作用域上的屬性。如果這個發生了,另外有個監聽器在監控被修改的屬性,有可能在同一個digest里面檢測不到這個變動:

http://jsbin.com/eTIpUyE/2/embed?js,console

我們來修復這個問題。

當數據臟的時候持續Digest

我們需要改變一下digest,讓它持續遍歷所有監聽器,直到監控的值停止變更。

首先,我們把現在的$digest函數改名為$$digestOnce,它把所有的監聽器運行一次,返回一個布爾值,表示是否還有變更了:

  1. Scope.prototype.$$digestOnce = function() { 
  2.   var self  = this
  3.   var dirty; 
  4.   _.forEach(this.$$watchers, function(watch) { 
  5.     var newValue = watch.watchFn(self); 
  6.     var oldValue = watch.last; 
  7.     if (newValue !== oldValue) { 
  8.       watch.listenerFn(newValue, oldValue, self); 
  9.       dirty = true
  10.     } 
  11.     watch.last = newValue
  12.   }); 
  13.   return dirty; 
  14. }; 

然后,我們重新定義$digest,它作為一個“外層循環”來運行,當有變更發生的時候,調用$$digestOnce:

  1. Scope.prototype.$digest = function() { 
  2.   var dirty; 
  3.   do { 
  4.     dirty = this.$$digestOnce(); 
  5.   } while (dirty); 
  6. }; 

$digest現在至少運行每個監聽器一次了。如果***次運行完,有監控值發生變更了,標記為dirty,所有監聽器再運行第二次。這會一直運行,直到所有監控的值都不再變化,整個局面穩定下來了。

Angular作用域里并不是真的有個函數叫做$$digestOnce,相反,digest循環都是包含在$digest里的。我們的目標更多是清晰度而不是性能,所以把內層循環封裝成了一個函數。

下面是新的實現:

http://jsbin.com/Imoyosa/3/embed?js,console

我們現在可以對Angular的監聽器有另外一個重要認識:它們可能在單次digest里面被執行多次。這也就是為什么人們經常說,監聽器應當是冪 等的:一個監聽器應當沒有邊界效應,或者邊界效應只應當發生有限次。比如說,假設一個監控函數觸發了一個Ajax請求,無法確定你的應用程序發了多少個請 求。

在我們現在的實現中,有一個明顯的遺漏:如果兩個監聽器互相監控了對方產生的變更,會怎樣?也就是說,如果狀態始終不會穩定?這種情況展示在下面的代碼里。在這個例子里,$digest調用被注釋掉了,把注釋去掉看看發生什么情況:

http://jsbin.com/eKEvOYa/3/embed?js,console

JSBin執行了一段時間之后就停止了(在我機器上大概跑了100,000次左右)。如果你在別的東西比如Node.js里跑,它會一直運行下去。

#p#

放棄不穩定的digest

我們要做的事情是,把digest的運行控制在一個可接受的迭代數量內。如果這么多次之后,作用域還在變更,就勇敢放手,宣布它永遠不會穩定。在這個點上,我們會拋出一個異常,因為不管作用域的狀態變成怎樣,它都不太可能是用戶想要的結果。

迭代的***值稱為TTL(short for Time To Live)。這個值默認是10,可能有點小(我們剛運行了這個digest 100,000次?。?,但是記住這是一個性能敏感的地方,因為digest經常被執行,而且每個digest運行了所有的監聽器。用戶也不太可能創建10 個以上鏈狀的監聽器。

事實上,Angular里面的TTL是可以調整的。我們將在后續文章討論provider和依賴注入的時候再回顧這個話題。

我們繼續,給外層digest循環添加一個循環計數器。如果達到了TTL,就拋出異常:

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   do { 
  5.     dirty = this.$$digestOnce(); 
  6.     if (dirty && !(ttl--)) { 
  7.       throw "10 digest iterations reached"; 
  8.     } 
  9.   } while (dirty); 
  10. }; 

下面是更新過的版本,可以讓我們循環引用的監控例子拋出異常:

http://jsbin.com/uNapUWe/2/embed?js,console

這些應當已經把digest的事情說清楚了。

現在,我們把注意力轉到如何檢測變更上吧。

基于值的臟檢查

我們曾經使用嚴格等于操作符(===)來比較新舊值,在絕大多數情況下,它是不錯的,比如所有的基本類型(數字,字符串等等),也可以檢測一個對象 或者數組是否變成新的了,但Angular還有一種辦法來檢測變更,用于檢測當對象或者數組內部產生變更的時候。那就是:可以監控值的變更,而不是引用。

這類臟檢查需要給$watch函數傳入第三個布爾類型的可選參數當標志來開啟。當這個標志為真的時候,基于值的檢查開啟。我們來重新定義$watch,接受這個參數,并且把它存在監聽器里:

  1. Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 
  2.   var watcher = { 
  3.     watchFn: watchFn, 
  4.     listenerFn: listenerFn, 
  5.     valueEq: !!valueEq 
  6.   }; 
  7.   this.$$watchers.push(watcher); 
  8. }; 

我們所做的一切是把這個標志加在監聽器上,通過兩次取反,強制轉換為布爾類型。當用戶調用$watch,沒傳入第三個參數的時候,valueEq會是未定義的,在監聽器對象里就變成了false。

基于值的臟檢查意味著如果新舊值是對象或者數組,我們必須遍歷其中包含的所有內容。如果它們之間有任何差異,監聽器就臟了。如果該值包含嵌套的對象或者數組,它也會遞歸地按值比較。

Angular內置了自己的相等檢測函數,但是我們會用Lo-Dash提供的那個。讓我們定義一個新函數,取兩個值和一個布爾標志,并比較相應的值:

  1. Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 
  2.   if (valueEq) { 
  3.     return _.isEqual(newValue, oldValue); 
  4.   } else { 
  5.     return newValue === oldValue; 
  6.   } 
  7. }; 

為了提示值的變化,我們也需要改變之前在每個監聽器上存儲舊值的方式。只存儲當前值的引用是不夠的,因為在這個值內部發生的變更也會生效到它的引用 上,$$areEqual方法比較同一個值的兩個引用始終為真,監控不到變化,因此,我們需要建立當前值的深拷貝,并且把它們儲存起來。

就像相等檢測一樣,Angular也內置了自己的深拷貝函數,但我們還是用Lo-Dash提供的。我們修改一下$digestOnce,在內部使用新的$$areEqual函數,如果需要的話,也復制***一次的引用:

  1. Scope.prototype.$$digestOnce = function() { 
  2.   var self  = this
  3.   var dirty; 
  4.   _.forEach(this.$$watchers, function(watch) { 
  5.     var newValue = watch.watchFn(self); 
  6.     var oldValue = watch.last; 
  7.     if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { 
  8.       watch.listenerFn(newValue, oldValue, self); 
  9.       dirty = true
  10.     } 
  11.     watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); 
  12.   }); 
  13.   return dirty; 
  14. }; 

現在我們可以看到兩種臟檢測方式的差異:

http://jsbin.com/ARiWENO/3/embed?js,console

相比檢查引用,檢查值的方式顯然是一個更為復雜的操作。遍歷嵌套的數據結構很花時間,保持深拷貝的數據也占用不少內存。這就是Angular默認不使用基于值的臟檢測的原因,用戶需要顯式設置這個標記去打開它。

Angular也提供了第三種臟檢測的方法:集合監控。就像基于值的檢測,也能提示對象和數組中的變更。但不同于基于值的 檢測方式,它做的是一個比較淺的檢測,并不遞歸進入到深層去,所以它比基于值的檢測效率更高。集合檢測是通過“$watchCollection”函數來 使用的,在這個系列的后續部分,我們會來看看它是如何實現的。

在我們完成值的比對之前,還有些JavaScript怪事要處理一下。

非數字(NaN)

在JavaScript里,NaN(Not-a-Number)并不等于自身,這個聽起來有點怪,但確實就這樣。如果我們在臟檢測函數里不顯式處理NaN,一個值為NaN的監聽器會一直是臟的。

對于基于值的臟檢測來說,這個事情已經被Lo-Dash的isEqual函數處理掉了。對于基于引用的臟檢測來說,我們需要自己處理。來修改一下$$areEqual函數的代碼:

  1. Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 
  2.   if (valueEq) { 
  3.     return _.isEqual(newValue, oldValue); 
  4.   } else { 
  5.     return newValue === oldValue || 
  6.       (typeof newValue === 'number' && typeof oldValue === 'number' && 
  7.        isNaN(newValue) && isNaN(oldValue)); 
  8.   } 
  9. }; 

現在有NaN的監聽器也正常了:

http://jsbin.com/ijINaRA/2/embed?js,console

基于值的檢測實現好了,現在我們該把注意力集中到應用程序代碼如何跟作用域打交道上了。

#p#

$eval – 在作用域的上下文上執行代碼

在Angular中,有幾種方式可以在作用域的上下文上執行代碼,最簡單的一種就是$eval。它使用一個函數作參數,所做的事情是立即執行這個傳 入的函數,并且把作用域自身當作參數傳遞給它,返回的是這個函數的返回值。$eval也可以有第二個參數,它所做的僅僅是把這個參數傳遞給這個函數。

$eval的實現很簡單:

  1. Scope.prototype.$eval = function(expr, locals) { 
  2.   return expr(this, locals); 
  3. }; 

$eval的使用一樣很簡單:

http://jsbin.com/UzaWUC/1/embed?js,console

那么,為什么要用這么一種明顯很多余的方式去執行一個函數呢?有人覺得,有些代碼是專門與作用域的內容打交道的,$eval讓這一切更加明顯。$scope也是構建$apply的一個部分,后面我們就來講它。

然后,可能$eval最有意思的用法是當我們不傳入函數,而是表達式。就像$watch一樣,可以給$eval一個字符串表達式,它會把這個表達式編譯,然后在作用域的上下文中執行。我們將在這個系列的后面部分實現這些。

$apply – 集成外部代碼與digest循環

可能Scope上所有函數里最有名的就是$apply了。它被譽為將外部庫集成到Angular的最標準的方式,這話有個不錯的理由。

$apply使用函數作參數,它用$eval執行這個函數,然后通過$digest觸發digest循環。下面是一個簡單的實現:

  1. Scope.prototype.$apply = function(expr) { 
  2.   try { 
  3.     return this.$eval(expr); 
  4.   } finally { 
  5.     this.$digest(); 
  6.   } 
  7. }; 

$digest的調用放置于finally塊中,以確保即使函數拋出異常,也會執行digest。

關于$apply,大的想法是,我們可以執行一些與Angular無關的代碼,這些代碼也還是可以改變作用域上的東西,$apply可以保證作用域 上的監聽器可以檢測這些變更。當人們談論使用$apply集成代碼到“Angular生命周期”的時候,他們指的就是這個事情,也沒什么比這更重要的了。

這里是$apply的實踐:

http://jsbin.com/UzaWUC/2/embed?js,console

延遲執行 – $evalAsync

在JavaScript中,經常會有把一段代碼“延遲”執行的情況 – 把它的執行延遲到當前的執行上下文結束之后的未來某個時間點。最常見的方式就是調用setTimeout()函數,傳遞一個0(或者非常?。┳鳛檠舆t參數。

這種模式也適用于Angular程序,但更推薦的方式是使用$timeout服務,并且使用$apply把要延遲執行的函數集成到digest生命周期。

但在Angular中還有一種延遲代碼的方式,那就是Scope上的$evalAsync函數。$evalAsync接受一個函數,把它列入計劃, 在當前正持續的digest中或者下一次digest之前執行。舉例來說,你可以在一個監聽器的監聽函數中延遲執行一些代碼,即使它已經被延遲了,仍然會 在現有的digest遍歷中被執行。

我們首先需要的是存儲$evalAsync列入計劃的任務,可以在Scope構造函數中初始化一個數組來做這事:

  1. function Scope() { 
  2.   this.$$watchers = []; 
  3.   this.$$asyncQueue = []; 

我們再來定義$evalAsync,它添加將在這個隊列上執行的函數:

  1. Scope.prototype.$evalAsync = function(expr) { 
  2.   this.$$asyncQueue.push({scope: this, expression: expr}); 
  3. }; 

我們顯式在放入隊列的對象上設置當前作用域,是為了使用作用域的繼承,在這個系列的下一篇文章中,我們會討論這個。

然后,我們在$digest中要做的***件事就是從隊列中取出每個東西,然后使用$eval來觸發所有被延遲執行的函數:

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   do { 
  5.     while (this.$$asyncQueue.length) { 
  6.       var asyncTask = this.$$asyncQueue.shift(); 
  7.       this.$eval(asyncTask.expression); 
  8.     } 
  9.     dirty = this.$$digestOnce(); 
  10.     if (dirty && !(ttl--)) { 
  11.       throw "10 digest iterations reached"; 
  12.     } 
  13.   } while (dirty); 
  14. }; 

這個實現保證了:如果當作用域還是臟的,就想把一個函數延遲執行,那這個函數會在稍后執行,但還處于同一個digest中。

下面是關于如何使用$evalAsync的一個示例:

http://jsbin.com/ilepOwI/1/embed?js,console

#p#

作用域階段

$evalAsync做的另外一件事情是:如果現在沒有其他的$digest在運行的話,把給定的$digest延遲執行。這意味著,無論什么時候調用$evalAsync,可以確定要延遲執行的這個函數會“很快”被執行,而不是等到其他什么東西來觸發一次digest。

需要有一種機制讓$evalAsync來檢測某個$digest是否已經在運行了,因為它不想影響到被列入計劃將要執行的那個。為此,Angular的作用域實現了一種叫做階段(phase)的東西,它就是作用域上一個簡單的字符串屬性,存儲了現在正在做的信息。

在Scope的構造函數里,我們引入一個叫$$phase的字段,初始化為null:

  1. function Scope() { 
  2.   this.$$watchers = []; 
  3.   this.$$asyncQueue = []; 
  4.   this.$$phase = null

然后,我們定義一些方法用于控制這個階段變量:一個用于設置,一個用于清除,也加個額外的檢測,以確保不會把已經激活狀態的階段再設置一次:

  1. Scope.prototype.$beginPhase = function(phase) { 
  2.   if (this.$$phase) { 
  3.     throw this.$$phase + ' already in progress.'; 
  4.   } 
  5.   this.$$phasephase = phase; 
  6. }; 
  7.   
  8. Scope.prototype.$clearPhase = function() { 
  9.   this.$$phase = null
  10. }; 
  11.  

在$digest方法里,我們來從外層循環設置階段屬性為“$digest”:

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   this.$beginPhase("$digest"); 
  5.   do { 
  6.     while (this.$$asyncQueue.length) { 
  7.       var asyncTask = this.$$asyncQueue.shift(); 
  8.       this.$eval(asyncTask.expression); 
  9.     } 
  10.     dirty = this.$$digestOnce(); 
  11.     if (dirty && !(ttl--)) { 
  12.       this.$clearPhase(); 
  13.       throw "10 digest iterations reached"; 
  14.     } 
  15.   } while (dirty); 
  16.   this.$clearPhase(); 
  17. }; 

我們把$apply也修改一下,在它里面也設置個跟自己一樣的階段。在調試的時候,這個會有些用:

  1. Scope.prototype.$apply = function(expr) { 
  2.   try { 
  3.     this.$beginPhase("$apply"); 
  4.     return this.$eval(expr); 
  5.   } finally { 
  6.     this.$clearPhase(); 
  7.     this.$digest(); 
  8.   } 
  9. }; 

最終,把對$digest的調度放進$evalAsync。它會檢測作用域上現有的階段變量,如果沒有(也沒有已列入計劃的異步任務),就把這個digest列入計劃。

  1. Scope.prototype.$evalAsync = function(expr) { 
  2.   var self = this
  3.   if (!self.$$phase && !self.$$asyncQueue.length) { 
  4.     setTimeout(function() { 
  5.       if (self.$$asyncQueue.length) { 
  6.         self.$digest(); 
  7.       } 
  8.     }, 0); 
  9.   } 
  10.   self.$$asyncQueue.push({scope: self, expression: expr}); 
  11. }; 

有了這個實現之后,不管何時、何地,調用$evalAsync,都可以確定有一個digest會在不遠的將來發生。

http://jsbin.com/iKeSaGi/1/embed?js,console

在digest之后執行代碼 – $$postDigest

還有一種方式可以把代碼附加到digest循環中,那就是把一個$$postDigest函數列入計劃。

在Angular中,函數名字前面有雙美元符號表示它是一個內部的東西,不是應用開發人員應該用的。但它確實存在,所以我們也要把它實現出來。

就像$evalAsync一樣,$$postDigest也能把一個函數列入計劃,讓它“以后”運行。具體來說,這個函數將在下一次digest完 成之后運行。將一個$$postDigest函數列入計劃不會導致一個digest也被延后,所以這個函數的執行會被推遲到直到某些其他原因引起一次 digest。顧名思義,$$postDigest函數是在digest之后運行的,如果你在$$digest里面修改了作用域,需要手動調 用$digest或者$apply,以確保這些變更生效。

首先,我們給Scope的構造函數加隊列,這個隊列給$$postDigest函數用:

  1. function Scope() { 
  2.   this.$$watchers = []; 
  3.   this.$$asyncQueue = []; 
  4.   this.$$postDigestQueue = []; 
  5.   this.$$phase = null

然后,我們把$$postDigest也加上去,它所做的就是把給定的函數加到隊列里:

  1. Scope.prototype.$$postDigest = function(fn) { 
  2.   this.$$postDigestQueue.push(fn); 
  3. }; 

最終,在$digest里,當digest完成之后,就把隊列里面的函數都執行掉。

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   this.$beginPhase("$digest"); 
  5.   do { 
  6.     while (this.$$asyncQueue.length) { 
  7.       var asyncTask = this.$$asyncQueue.shift(); 
  8.       this.$eval(asyncTask.expression); 
  9.     } 
  10.     dirty = this.$$digestOnce(); 
  11.     if (dirty && !(ttl--)) { 
  12.       this.$clearPhase(); 
  13.       throw "10 digest iterations reached"; 
  14.     } 
  15.   } while (dirty); 
  16.   this.$clearPhase(); 
  17.   
  18.   while (this.$$postDigestQueue.length) { 
  19.     this.$$postDigestQueue.shift()(); 
  20.   } 
  21. }; 

下面是關于如何使用$$postDigest函數的:

http://jsbin.com/IMEhowO/1/embed?js,console

#p#

異常處理

現有對Scope的實現已經逐漸接近在Angular中實際的樣子了,但還有些脆弱,因為我們迄今為止沒有花精力在異常處理上。

Angular的作用域在遇到錯誤的時候是非常健壯的:當產生異常的時候,不管在監控函數中,在$evalAsync函數中,還是在$$postDigest函數中,都不會把digest終止掉。我們現在的實現里,在以上任何地方產生異常都會把整個$digest弄掛。

我們可以很容易修復它,把上面三個調用包在try…catch中就好了。

Angular實際上是把這些異常拋給了它的$exceptionHandler服務。既然我們現在還沒有這東西,先扔到控制臺上吧。

$evalAsync和$$postDigest的異常處理是在$digest函數里,在這些場景里,從已列入計劃的程序中拋出的異常將被記錄成日志,它后面的還是正常運行:

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   this.$beginPhase("$digest"); 
  5.   do { 
  6.     while (this.$$asyncQueue.length) { 
  7.       try { 
  8.         var asyncTask = this.$$asyncQueue.shift(); 
  9.         this.$eval(asyncTask.expression); 
  10.       } catch (e) { 
  11.         (console.error || console.log)(e); 
  12.       } 
  13.     } 
  14.     dirty = this.$$digestOnce(); 
  15.     if (dirty && !(ttl--)) { 
  16.       this.$clearPhase(); 
  17.       throw "10 digest iterations reached"; 
  18.     } 
  19.   } while (dirty); 
  20.   this.$clearPhase(); 
  21.   
  22.   while (this.$$postDigestQueue.length) { 
  23.     try { 
  24.       this.$$postDigestQueue.shift()(); 
  25.     } catch (e) { 
  26.       (console.error || console.log)(e); 
  27.     } 
  28.   } 
  29. }; 

監聽器的異常處理放在$$digestOnce里。

  1. Scope.prototype.$$digestOnce = function() { 
  2.   var self  = this
  3.   var dirty; 
  4.   _.forEach(this.$$watchers, function(watch) { 
  5.     try { 
  6.       var newValue = watch.watchFn(self); 
  7.       var oldValue = watch.last; 
  8.       if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { 
  9.         watch.listenerFn(newValue, oldValue, self); 
  10.         dirty = true
  11.       } 
  12.       watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); 
  13.     } catch (e) { 
  14.       (console.error || console.log)(e); 
  15.     } 
  16.   }); 
  17.   return dirty; 
  18. }; 

現在我們的digest循環碰到異常的時候健壯多了。

http://jsbin.com/IMEhowO/2/embed?js,console

銷毀一個監聽器

當注冊一個監聽器的時候,一般都需要讓它一直存在于整個作用域的生命周期,所以很少會要顯式把它移除。也有些場景下,需要保持作用域的存在,但要把某個監聽器去掉。

Angular中的$watch函數是有返回值的,它是個函數,如果執行,就把剛注冊的這個監聽器銷毀。想在我們這個版本里實現這功能,只要返回一個函數在里面把這個監控器從$$watchers數組去除就可以了:

  1. Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 
  2.   var self = this
  3.   var watcher = { 
  4.     watchFn: watchFn, 
  5.     listenerFn: listenerFn, 
  6.     valueEq: !!valueEq 
  7.   }; 
  8.   self.$$watchers.push(watcher); 
  9.   return function() { 
  10.     var index = self.$$watchers.indexOf(watcher); 
  11.     if (index >= 0) { 
  12.       self.$$watchers.splice(index, 1); 
  13.     } 
  14.   }; 
  15. }; 

現在我們就可以把$watch的這個返回值存起來,以后調用它來移除這個監聽器:

http://jsbin.com/IMEhowO/4/embed?js,console

展望未來

我們已經走了很長一段路了,已經有了一個***可以運行的類似Angular這樣的臟檢測作用域系統的實現了,但是Angular的作用域上面還做了更多東西。

或許最重要的是,在Angular里,作用域并不是孤立的對象,作用域可以繼承于其他作用域,監聽器也不僅僅是監聽本作用域上的東西,還可以監聽這 個作用域的父級作用域。這種方法,概念上很簡單,但是對于初學者經常容易造成混淆。所以,本系列的下一篇文章主題就是作用域的繼承。

后面我們會討論Angular的事件系統,也是實現在Scope上的。

原文鏈接:http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.html

譯文鏈接:http://www.ituring.com.cn/article/39865#

責任編輯:陳四芳 來源: 圖靈社區
相關推薦

2015-09-06 09:17:07

AngularJS構建框架

2015-10-08 09:58:58

AngularJSiOS開發

2025-09-10 08:03:52

運維開發公共庫

2025-02-10 12:00:00

圖像分類OpenCVPython

2021-07-25 21:28:55

人臉識別人工智能工具

2020-03-06 20:50:30

FedoraLinuxNextcloud

2014-07-30 09:35:36

DockerPaaS

2011-09-09 10:49:18

2012-11-13 11:29:51

Ubuntu

2023-07-30 17:34:53

KV存儲ChunkPosit

2016-09-18 10:08:38

Linux發行版SUSE Studio

2015-09-06 10:58:36

PHP框架搭建結構

2015-10-26 13:41:41

Aleax查詢服務

2018-03-22 11:00:45

PythonRSS

2023-12-12 13:07:16

2018-04-23 13:10:01

2020-02-24 11:11:10

IT企業技術

2011-04-26 15:18:23

CentOS發行版

2020-04-09 14:23:44

PythonMarkdown編輯器

2022-06-23 09:44:01

LinuxLive CD
點贊
收藏

51CTO技術棧公眾號

一区二区三区四区日韩| 成人天堂yy6080亚洲高清 | 国产精品亚洲αv天堂无码| 免费在线性爱视频| 久久99精品国产麻豆婷婷| 欧美成人一区二区三区电影| 理论片大全免费理伦片| 欧美日一区二区三区| 亚洲曰韩产成在线| 午夜免费电影一区在线观看| 色噜噜在线播放| 九色porny丨国产精品| …久久精品99久久香蕉国产| 性欧美疯狂猛交69hd| 亚州国产精品| 日韩精品一区二区三区在线| 激情视频综合网| 在线黄色网页| 国产精品午夜春色av| 久久精品国产第一区二区三区最新章节| 夜夜嗨av禁果av粉嫩avhd| 国产亚洲精品bv在线观看| 久久电影一区二区| 日本综合在线观看| 亚洲丁香日韩| 亚洲国产精品系列| gogo亚洲国模私拍人体| 国产福利亚洲| 日韩欧美在线国产| 午夜精品久久久久久久无码| 性欧美videos高清hd4k| 最新国产成人在线观看| 日韩视频在线播放| 青青草免费在线视频| 成人夜色视频网站在线观看| 91夜夜揉人人捏人人添红杏| 亚洲国产无线乱码在线观看| 久久一区激情| 69久久夜色精品国产69乱青草| 男女免费视频网站| 中文字幕一区二区三区在线视频| 色婷婷av一区二区三区在线观看| 亚洲一级中文字幕| 一区二区三区视频免费观看 | 极品白浆推特女神在线观看| 成人精品国产福利| 高清视频一区| 国产18精品乱码免费看| 国产传媒久久文化传媒| 亚洲在线免费视频| 午夜精品久久久久久久第一页按摩 | 26uuu成人网一区二区三区| 国产精品sss| 丰满熟妇乱又伦| 成人免费观看男女羞羞视频| 国产专区一区二区| 日色在线视频| 国产喂奶挤奶一区二区三区| 亚洲ai欧洲av| 大片免费在线观看| 中文字幕亚洲成人| 国产精品8888| a级片在线免费| 精品国产91久久久| 国产xxxxx在线观看| 欧洲精品一区二区三区| 欧美午夜精品免费| 欧美激情国内自拍| 在线日韩成人| 亚洲美女av在线| 一级二级黄色片| 91精品国产视频| 欧美精品成人91久久久久久久| 日韩高清免费av| 性娇小13――14欧美| 国产极品jizzhd欧美| 亚洲综合精品视频| 国产·精品毛片| 蜜桃成人免费视频| 日日夜夜精品一区| 亚洲午夜在线视频| 九九热在线免费| 日韩免费一级| 亚洲精品小视频| 黄色片子在线观看| 国产亚洲欧洲| 91性高湖久久久久久久久_久久99| 欧美一级在线免费观看| 国产精品视频yy9299一区| 超碰10000| 电影亚洲一区| 亚洲国产成人在线视频| 亚洲精品成人av久久| 激情一区二区| 国产精品中文字幕在线| 国精产品乱码一区一区三区四区| 久久久午夜精品理论片中文字幕| 国产一区一区三区| 成人欧美大片| 欧美成人video| 免费看日本黄色片| 精品动漫av| 国产精品视频yy9099| 少妇高潮久久久| 国产精品护士白丝一区av| 黄色片网址在线观看| 九九热这里有精品| 日韩精品视频观看| 麻豆一区产品精品蜜桃的特点| 日韩精品1区2区3区| 国产三区精品| 亚洲性图自拍| 欧美另类z0zxhd电影| 韩国女同性做爰三级| 亚洲精品四区| 97se亚洲综合在线| 天堂资源在线中文| 91黄色免费网站| 小毛片在线观看| 欧美激情四色| 成人中心免费视频| 在线视频三区| 欧美中文字幕不卡| 丰腴饱满的极品熟妇| 亚洲国产精品一区| av观看久久| 性国产高清在线观看| 777午夜精品免费视频| 极品尤物一区二区| 视频一区欧美日韩| 欧美一级二级三级| 涩涩视频网站在线观看| 亚洲第一视频网| 久久久久久久久久久97| 国产一区二区按摩在线观看| 亚洲砖区区免费| 国产精品第一国产精品| 亚洲人成网7777777国产| 日韩少妇裸体做爰视频| 成人精品国产免费网站| 成人精品视频在线播放| 一区二区三区四区视频免费观看| 久久不射热爱视频精品| 99在线观看免费| 亚洲精品视频一区| www.偷拍.com| 伊人成人在线视频| 国产日韩二区| 午夜伦理福利在线| 亚洲精品一区中文| 免费视频网站在线观看入口| 久久精品人人做人人综合| 97在线播放视频| 欧美日韩xxxx| 国产精品偷伦一区二区| 蜜桃视频在线观看www社区 | 在线观看免费高清完整| 欧美日韩精品高清| 天天看片中文字幕| 岛国精品在线播放| 日本日本19xxxⅹhd乱影响| 亚洲人挤奶视频| 国产成人在线视频| 日本在线观看视频| 日韩午夜激情视频| 日韩av在线播放观看| 久久日韩粉嫩一区二区三区| 别急慢慢来1978如如2| 成人羞羞网站入口| 亚洲mm色国产网站| 99久久精品免费看国产小宝寻花| 亚洲国产天堂久久综合网| 国产又大又黄又粗| 日本一区二区高清| 超碰91在线播放| 亚洲三级网站| 午夜精品电影在线观看| 精品一区二区三区中文字幕在线 | 中文字幕日韩一区二区三区不卡| 国产精品视频一区视频二区 | 美女日韩一区| 97精品免费视频| aaa在线免费观看| 欧美成va人片在线观看| 国产一区二区三区影院| 国产精品卡一卡二卡三| 欧美xxxxx精品| 蜜臀av国产精品久久久久| 国产a级黄色大片| 国产乱码精品一区二区三区四区| 国产日韩欧美视频在线| 狼人综合视频| 久久精品成人欧美大片| 天天舔天天干天天操| 91.com在线观看| 一二三区免费视频| 亚洲大尺度视频在线观看| 卡一卡二卡三在线观看| 福利一区福利二区| 欧美特级aaa| 亚洲精品欧洲| 亚洲第一页在线视频| 亚洲精品动态| 肥熟一91porny丨九色丨| 国产一区二区色噜噜| 欧美有码在线视频| 性欧美video高清bbw| 中文字幕欧美精品日韩中文字幕| 婷婷开心激情网| 日韩午夜av电影| 中文字幕精品一区二区精| 午夜视频在线观看一区二区 | av资源一区二区| 亚洲欧美一级| 国产精品久久久| 性感女国产在线| 久久久久久久国产| av片在线观看网站| 日韩网站在线观看| 美州a亚洲一视本频v色道| 亚洲国产精久久久久久| 韩国av电影在线观看| 日韩视频免费观看高清完整版在线观看| 婷婷激情五月网| 午夜不卡在线视频| 精品处破女学生| 一片黄亚洲嫩模| 国产精品久久久久久久精| 亚洲欧洲另类国产综合| 免费黄在线观看| 国产视频亚洲色图| caopeng视频| 国产午夜精品一区二区三区四区 | 国产精品白丝在线| gv天堂gv无码男同在线观看| 国产欧美一区二区精品婷婷| 插吧插吧综合网| 91在线观看视频| 中国美女乱淫免费看视频| 91视频一区二区| 亚洲天堂网一区二区| 久久久久一区二区三区四区| 蜜桃av免费看| 欧美高清在线精品一区| 蜜桃av免费观看| 国产精品久久久久久久久久久免费看| 亚洲av毛片基地| 中文字幕中文字幕在线一区 | 久久人人爽人人人人片| 91在线丨porny丨国产| 内射中出日韩无国产剧情| 99re这里都是精品| 大又大又粗又硬又爽少妇毛片| 久久夜色精品一区| 亚洲av成人无码久久精品| 国产精品每日更新| 欧美成人777| 亚洲地区一二三色| 9i精品福利一区二区三区| 色欧美日韩亚洲| 91麻豆一区二区| 日韩一级片网站| 天堂中文在线视频| 一本色道久久综合亚洲精品小说| 色综合久久影院| 久久香蕉国产线看观看网| 欧美videosex性欧美黑吊| 国产91精品高潮白浆喷水| 精品视频一区二区三区四区五区| 国产欧美 在线欧美| 日韩视频一二区| 久久综合毛片| 99精品视频在线观看播放| 久久亚洲a v| 日韩电影一区二区三区四区| 欧美日韩理论片| 99热99精品| 亚洲一级理论片| 亚洲国产精品影院| 麻豆精品久久久久久久99蜜桃| 欧美日韩国产片| 日韩一卡二卡在线| 中文字幕视频在线免费欧美日韩综合在线看 | 视频在线亚洲| 欧美亚州在线观看| 欧美1区免费| 久久久久久三级| 成人黄色国产精品网站大全在线免费观看| 人妻精品久久久久中文字幕| 国产精品久久福利| 国产无套丰满白嫩对白| 欧美另类videos死尸| 水中色av综合| 免费91在线视频| 蜜桃成人精品| 精品人伦一区二区三区| 无需播放器亚洲| 中文字幕在线观看第三页| 成年人网站91| 1024手机在线视频| 欧美日韩视频在线第一区| 日韩一级中文字幕| 超碰日本道色综合久久综合| 欧美电影网站| 国产一区喷水| 午夜视频一区| 尤物国产在线观看| 久久一区二区三区国产精品| 久一视频在线观看| 欧美久久久久免费| 国产高清一区在线观看| 97热在线精品视频在线观看| 国产午夜精品一区在线观看| 日韩精品一区二区三区色偷偷| 亚洲小说欧美另类社区| 三级黄色片免费看| 亚洲欧美综合色| 凹凸精品一区二区三区| 日韩精品在线免费观看视频| 四虎影视成人| 亚洲一区久久久| 99久久www免费| 高清一区在线观看| 欧美激情在线免费观看| 黄色在线视频网址| 亚洲欧美成人在线| 综合毛片免费视频| 久久精品日产第一区二区三区乱码| 午夜日韩视频| avtt中文字幕| 亚洲激情欧美激情| 精品乱子伦一区二区| 久久中国妇女中文字幕| 伊人久久综合网另类网站| 亚洲一卡二卡三卡| 久久精品99国产精品日本| www.99re6| 91精品国产综合久久久蜜臀图片 | 91成人在线播放| 香蕉久久精品| 999精品网站| 国产午夜精品久久久久久免费视 | 欧美成人aaa片一区国产精品| 91精品婷婷国产综合久久竹菊| av在线免费播放网站| 国产美女被下药99| 欧美xxxxx视频| 国产欧美激情视频| 一区二区三区四区蜜桃| 国产成人自拍一区| 69av成年福利视频| 精品毛片免费观看| 手机av在线网| 国产毛片欧美毛片久久久| 欧美三级中文字幕| 精产国品自在线www| 亚洲一区二区久久久久久| 欧美日韩一卡| 亚洲图片综合网| 色婷婷狠狠综合| 18视频免费网址在线观看| 91久久久久久| 极品少妇一区二区三区| 手机av免费看| 欧美男男青年gay1069videost| 国产激情在线观看| 国产三级精品在线不卡| 日韩精品视频网| 欧美丰满艳妇bbwbbw| 亚洲精品国偷自产在线99热| 日本综合久久| 熟女视频一区二区三区| 99久久国产综合色|国产精品| 四虎精品永久在线| 中文字幕亚洲综合久久筱田步美| 国产欧美视频在线| 国产伦精品一区二区三区四区视频_| www国产精品av| 一区二区日韩在线观看| 欧美老肥婆性猛交视频| 欧美一区 二区| 激情文学亚洲色图| 午夜视黄欧洲亚洲| 一区二区高清不卡| 国产麻豆日韩| 蜜臀av亚洲一区中文字幕| 国产在线观看免费av| 国产亚洲综合久久| 亚洲91网站| 免费观看成人在线视频| 一区二区三区在线播放| 久久天堂电影| 成人看片在线| 免费高清成人在线| 亚洲欧美在线观看视频| 久久精品国产亚洲精品| 夜夜春成人影院| 亚洲成人精品在线播放| 欧美写真视频网站| 成人影院在线视频|