Angular原生實現了兩個工具類:DefaultKeyValueDiffer和DefaultIterableDiffer,它們分別用來檢查兩個對象或兩個數組之間的差異(也就是diff)。典型的使用場景是:檢查某個變量在兩個時刻之間是否發生了改變、發生了什麼樣的改變,在這篇文章中,咱們稱它爲變動檢測。html
請將diff與change detection區分開來。
Angular的變化檢測默認只比較對象的引用是否改變,可是咱們能夠經過DoCheck生命週期鉤子來作一些額外的檢測,好比檢查對象是否增長刪除改動了某些屬性、檢查數組是否增長刪除移動了某些條目。這個時候變動檢測就能夠派上用場了。
舉個例子,NgForOf指令內部就是經過DefaultIterableDiffer
來檢測輸入數組發生了怎樣的變化,從而可以用最小的代價去更新DOM。git
這兩個工具類中包含的算法能夠說是十分通用的,甚至能夠移植到其餘框架、語言去。除此以外,掌握這種變動檢測算法也可以幫助咱們更好地理解、使用NgForOf
,甚至編寫本身的結構型指令。github
在咱們經過源碼瞭解它們的算法以前,我先簡單地介紹一下Differ是如何使用的。算法
要使用這兩個工具類,並不須要(也不該該)本身建立這兩個類的實例,BrowserModule已經將將它們註冊在注入器中。segmentfault
如下代碼展現瞭如何獲取和使用DefaultKeyValueDiffer:api
import { Component, KeyValueDiffers, KeyValueDiffer } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { constructor(keyValueDiffers: KeyValueDiffers) { const someObj: any = { a: 1, b: 2 }; console.log('KeyValueDiffers"', keyValueDiffers); const defaultKeyValueDifferFactory = keyValueDiffers.find(someObj); console.log('defaultKeyValueDifferFactory:', defaultKeyValueDifferFactory); console.log('test defaultKeyValueDifferFactory.supports:', defaultKeyValueDifferFactory.supports({}), defaultKeyValueDifferFactory.supports([]), defaultKeyValueDifferFactory.supports('string') ) const defaultKeyValueDiffer = defaultKeyValueDifferFactory.create(); console.log('defaultKeyValueDiffer:', defaultKeyValueDiffer); const changes1 = defaultKeyValueDiffer.diff(someObj); console.log('changes1:') changes1.forEachAddedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); console.log('--------------------') delete someObj.a; someObj.c = 'new value'; const changes2 = defaultKeyValueDiffer.diff(someObj); console.log('changes2:') changes2.forEachAddedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); changes2.forEachRemovedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); console.log('--------------------') } }
DefaultIterableDiffer的使用是徹底相似的。你也能夠參考api文檔。數組
你能夠從這個例子初步體會到「抽象」的威力。使用者調用interface KeyValueDiffer定義的API,而徹底不知道(也不須要知道)背後DefaultKeyValueDiffer這個類的存在。app
更棒的是,咱們等一下能夠看到,咱們能夠本身實現特殊用途的KeyValueDiffer工具類(工具類實現這個接口),而後這個工具類就能被加入到KeyValueDiffers
中,從而能在應用的指定範圍內分發,所以這套系統(能夠命名爲「Differ供應系統」)具備很強的可擴展性。框架
先拋開DefaultKeyValueDiffer自己不談,咱們先從源碼來看看KeyValueDiffer供應系統是如何實現的。ide
這個系統主要由3個類或接口組成:KeyValueDiffers類, KeyValueDifferFactory接口, KeyValueDiffer接口。
從前面的使用示例能夠看出,使用者最開始須要經過依賴注入拿到KeyValueDiffers類的實例:
constructor(keyValueDiffers: KeyValueDiffers)
ApplicationModule已經註冊了這個服務的provider,咱們的AppModule在引入BrowserModule的時候會獲得這個provider。
有意思的是,Angular註冊的_keyValueDiffersFactory直接返回同一個KeyValueDiffers實例,所以,這個根KeyValueDiffers是全局惟一的,即便你在同一個頁面運行多個Angular程序。效果等同於在Platform Injector註冊了這個服務。
好,使用者已經能夠獲取到KeyValueDiffers實例了,它是幹什麼的呢?
KeyValueDiffers持有一些KeyValueDifferFactory,而且能夠經過find方法返回支持指定對象kv的Differ的工廠(某種Differ只支持某種特定的對象,好比說,咱們能夠實現一個專門支持Date的Differ)。
KeyValueDiffers的靜態方法create能夠在建立新實例的時候指定一個"parent",新的實例會得到parent擁有的factories,相似於繼承。注意到concat時,本身的factories在前面,parent的factories在後面,而find方法是從前日後查找的,所以find先查找本身擁有的factories,再檢查parent的factories。
KeyValueDiffers的靜態方法extend,註釋已經寫得很清楚了,而且源碼也很簡單,它是生成一個StaticProvider的工具函數。你能夠將KeyValueDiffers註冊在某個的依賴注入層級上,從而在此層級如下的組件、指令可以經過依賴注入獲取它。
在咱們經過find方法獲得KeyValueDifferFactory之後,能夠經過KeyValueDifferFactory.supports
檢查KeyValueDiffer是否支持某個對象的變動檢測,而後能夠經過KeyValueDifferFactory.create
得到新的KeyValueDiffer對象。顯然每種KeyValueDiffer必須有一個對應的KeyValueDifferFactory,好比DefaultKeyValueDiffer有本身的DefaultKeyValueDifferFactory。所以咱們在實現本身的Differ的時候要實現Factory和Differ來分別implements這兩個接口。
假設咱們不實現本身的KeyValueDiffer,從KeyValueDiffers獲取到DefaultKeyValueDifferFactory之後,直接調用DefaultKeyValueDifferFactory.create()
就能夠得到DefaultKeyValueDiffer對象,就像最前面的例子同樣。經過KeyValueDiffer.diff(obj)
能夠追蹤obj與上次調用diff傳入的obj相比,發生了哪些改變。至此,"KeyValueDiffer供應系統"的使命就完成了。
這套KeyValueDiffer供應系統有如下優勢:
find
的查找順序。讓咱們從源碼研究它。
注意到它同時實現了KeyValueDiffer和KeyValueChanges接口,所以這個類不只要發現新舊對象之間變動,並且要給用戶提供遍歷這些變化的API。
既然Differ要檢測「變化」,那麼它就要存儲狀態,也就是上次調用diff傳入的obj是怎麼樣的。從類成員能夠看出,每一個Differ對象要存儲obj的全部條目,分別經過Map和鏈表。用戶可以經過forEachItem
遍歷當前obj的全部屬性。
此外,爲了存儲有用的信息,還定義了4個鏈表,分別是_previousMapHead
(舊obj的全部屬性) _changesHead
_additionsHead
_removalsHead
。若是用戶想要獲取這4個信息,能夠分別調用forEachPreviousItem
(遍歷舊obj的全部屬性) forEachChangedItem
forEachAddedItem
forEachRemovedItem
來遍歷這些列表。
有這麼多的鏈表,爲了節約內存,一個鏈表條目,有各類不一樣的鏈表next指針,能夠同時做爲多個鏈表的成員。
剩下的全部函數都是圍繞diff
來服務的。能夠看到diff基本上至關於直接調用check。check就包含了變動檢測算法:
reset()
爲遷移到下一個狀態做準備(包括:更新_previousMapHead
鏈表,更新每一個record的 _nextPrevious
指針和previousValue
,清空_changesHead
_additionsHead
_removalsHead
鏈表)遍歷新傳入的obj
的每一個屬性,依次與_mapHead
比較(_mapHead
存儲的仍是舊obj的record)。
_changesHead
鏈表。_getOrCreateRecordForKey
方法中,先嘗試從_records Map找到這個key,若是找到了就比較其value是否與新obj中的value相同(若是不一樣的話就_addToChanges
),而後將它暫時從_mapHead鏈表中刪除,_getOrCreateRecordForKey
返回這個record(等一下會插入到鏈表的正確位置); 若是在Map找不到這個key,說明這是一個新加入的屬性,則建立一個新的record並加入_additionsHead
鏈表,_getOrCreateRecordForKey
返回這個新建的record(等一下再插入到_mapHead
鏈表中)。_getOrCreateRecordForKey
執行完畢之後,將返回的record插入_mapHead鏈表的正確位置。_mapHead
鏈表中還有還沒有訪問的record,這些record都是被刪除的。將它們從_mapHead
移除、加入_removalsHead
、從_records
中刪除這些條目、更新這些record的狀態。實現這個算法時要理清楚何時更新_changesHead _additionsHead _removalsHead鏈表,也就是什麼狀況意味着發現了change、addition、removal。這在上面的表述中已經說明了。
理清楚了這一點之後,剩下的就是維護鏈表的操做了。同時維護這麼多的鏈表確實是一件很容易出錯的事情。
IterableDiffer用來對數組或類數組對象進行變動檢測。
IterableDiffer供應系統與KeyValueDiffer供應系統很是相似,這裏只討論幾個比較重要的地方:
IterableChanges.forEachIdentityChange
可讓用戶看到全部trackById相同但Identity變化(至關於a!==b)的那些條目。那些簡單的,或者DefaultKeyValueDiffer
也有的類成員我就不一一介紹了。
與前面相似地,變動檢測的邏輯封裝在_check函數中。讓咱們從這裏開始。
this._reset()
進行初步的狀態更新。包括:更新_previousItHead
鏈表,更新每一個record的 _nextPrevious
指針,重置previousIndex
,清空_additionsHead
_movesHead
_removalsHead
_identityChangesHead
鏈表。Array.isArray(collection)
,因爲DefaultIterableDiffer支持一些類數組對象,所以在判斷不成功的時候會執行另外一種算法來檢測變動。咱們不妨假設檢測正常數組的變動。對於collection
(新數組)的每一個項,執行如下操做(用下標index
來遍歷collection
):
this._trackByFn(index, item)
計算當前項的標識值。**若是新舊數組之間的兩個項的標識值相等,咱們就認爲它們是同一個項,無論identity是否同樣(即無論a===b是否成立),咱們都認爲順序沒有變化。比較_itHead
鏈表(舊數組)的第index
個項(命名爲item1)與collection
的第index
個項(命名爲item2),它們標識值
是否相同:
若是不相同,調用_mismatch
來處理,使得item2成爲_itHead
鏈表的第index
個項:
_itHead
鏈表中刪除,畢竟它們沒有在正確的位置上(若是後面發現有這個項,再將它加到合適的位置)。而後將它加入_unlinkedRecords
中(它是_DuplicateMap類型,也就是MuitiMap的一種實現。之因此要用到MuitiMap,是由於數組中可能有多個項的標識值相同),而後將它加入_removalsHead
鏈表中。index
之後的項中找到有相同標識值的項。若是找到的話,就檢測到移動變動,因而要將這個項從_itHead
鏈表中原來的位置移動到index
位置,並加入_movesHead
鏈表。若是沒有在舊數組找到相同標識值的項,嘗試從_unlinkedRecords
找到相同標識值的項。若是找到的話,一樣檢測到移動變動。將這個項從_unlinkedRecords
Map和_removalsHead
鏈表移除(撤銷_addToRemovals
操做),而後插入到_itHead
鏈表的index
位置。若是從_unlinkedRecords
仍是沒找到相同標識值的項,說明這是一個新增長的項,因而將它插入_itHead
鏈表的index
位置並加入_additionsHead
鏈表。_mismatch
執行完畢之後。設置mayBeDirty = true
。這個標識表示未來每次檢測到item1
與item2
標識值相等得時候,要調用_verifyReinsertion
來修正某種錯誤,下面再談。_verifyReinsertion
來檢查前面步驟可能產生的插入順序錯誤:假設發生變動[a, a]
=> [b, a, a]
,那麼在對比鏈表中的a和新數組的b之後,會刪除鏈表中的a(鏈表存儲的是舊數組),而後插入新數組的b,接下來,鏈表中的下一個項依然是a,就會匹配新數組中的第一個a(舊數組的第二個a匹配新數組的第一個a),接下來會在鏈表的末尾reinsert
剛纔刪除的a(原數組的第一個a)。通過這樣的變動檢測之後,兩個a的順序變了。正確的作法應該是「將b插入數組0位置」,而不是「將數組0位置的a換成b,而後在數組末尾加入a」。_unlinkedRecords
中是否有相同標識值的項),若是有刪除過,則這個項才與item2對應,因而撤銷被刪除項的_addToRemovals
操做,並將這個項reinsert
到鏈表的index
位置。_verifyReinsertion
還有另外一個做用,你那就是檢查record.currentIndex
是否正確。假如在record
前面已經插入一個項並刪除一個項,那麼currentIndex不須要改變;可是若是隻是前面插入了1個,那麼插入項之後的全部項的currentIndex都要+1,而後記錄這個移動操做(_addToMoves)。在這種狀況下,雖說「這些項都移動了」不太準確,可是畢竟它們所在的下標都變化了,咱們仍是先記錄這些移動,之後調用forEachOperation
的時候會過濾掉這種不嚴格的移動。_itHead
鏈表後面還有沒訪問到的項,則這些項是被刪除的。使用_truncate從鏈表中刪掉它們,並記錄它們的刪除(_addToRemovals
)。_truncate
除此以外還作一些收尾工做:將檢測變動時用來查詢的_unlinkedRecords
Map清空(這些是被刪除的項,它們已經被執行_addToRemovals
了),而後將各類鏈表尾的next賦值爲null(咱們以前加入鏈表的時候都沒有考慮它是否是鏈表尾)。能夠看出,算法的重點在於第3步的for循環。for循環剛開始的時候,_itHead
鏈表仍是舊數組的狀態。而後通過一輪循環,就修改_itHead
鏈表,將正確的項移動到_itHead
鏈表的index位置。所以,這個for循環從左向右逐項更新_itHead
鏈表,使得它有愈來愈長的前綴與新數組匹配。
diff
執行完畢之後,變動的信息就存儲在DefaultIterableDiffer
的那些鏈表中了,用戶能夠經過IterableChanges.forEachOperation
獲得一系列數組操做(增長刪除移動),這些操做能將舊數組更新爲新數組。注意,這些數組操做是經過計算獲得的,不必定是實際發生在舊數組上的操做。forEachOperation
是如何經過變動信息計算出可能發生的操做序列呢?看源碼以前,首先應該思考它的思路是怎麼樣的,不然這段代碼會看得很是費勁。
發生在數組上的變動操做無非三種:增長項、刪除項、移動項(兩個項的交換能夠看做兩次移動)。其中,移動項又能夠分爲向前移動(下標變小)和向後移動(下標變大)。咱們以前已經提到過,將某個項向前移動時,它所「通過」的那些項的下標會+1。這種下標+1只是其餘移動的副產品,不該該算做真正的向後移動。好比對於變動[a,b,c]=>[c,a,b]
,咱們天然的想法是「c從2移動到0」,而bc下標的增長不該另算做變動。
進一步思考,若是項item的下標增長,其實全都是由於item後面的一些項移動到了item前面(如今僅考慮移動項,不考慮增長項)。也就是說,向後移動均可以替換爲其餘項的向前移動,咱們再也不須要考慮向後移動了。
舉個例子,[a,b,c,d]=>[d,c,b,a]
的變動操做序列是:d向前移動到0位置,c向前移動到1位置,b向前移動到2位置,a不須要本身移動!
forEachOperation
的計算操做序列算法能夠簡述以下(先只考慮移動項,不考慮有增長項和刪除項的狀況):
遍歷_itHead
鏈表(此時diff
已經執行完,_itHead
鏈表的順序與新數組相同),對於每一項record
,依次檢查其臨時下標和目標下標(在源碼中分別命名爲adjPreviousIndex
和currentIndex
)。臨時下標的意思是,舊數組剛執行完已計算出的操做所獲得的臨時狀態中,這個項的下標。目標下標的意思是,這個項在最終目標數組中的下標。好比,計算[a,b,c,d]=>[d,b,c,a]
的變動操做序列時,已經計算出「d移動到0,b移動到1」,舊數組執行完這兩個操做之後的臨時狀態爲[d,b,a,c]
,a的臨時下標爲2,c的臨時下標爲3,目標下標始終分別是3和2。
adjPreviousIndex===currentIndex
,說明在當前狀態中,這個項剛好處於目標位置,不須要移動。adjPreviousIndex>currentIndex
,說明在當前狀態中,這個項須要被向前移動,才能到達目標位置。這個if就是判斷這個狀況的。adjPreviousIndex<currentIndex
的狀況。其實adjPreviousIndex
和currentIndex
分別表示【不忽略增長、刪除項狀況下的】臨時下標和目標下標。經過如下兩個減法,能計算出【忽略增長、刪除項的狀況下的——也就是說假設被增長、刪除的項歷來都不存在】臨時下標和目標下標:
const localMovePreviousIndex = adjPreviousIndex - addRemoveOffset; const localCurrentIndex = currentIndex ! - addRemoveOffset;
由於addRemoveOffset
變量記錄了到目前爲止的計算中,已經增長了多少個項(若是刪除的項比增長的多,則這個值爲負數),因此減掉這個數之後就是(忽略被增長的項的狀況下的)臨時下標和目標下標。
那麼adjPreviousIndex
(臨時下標)是如何獲得的呢?adjPreviousIndex的計算函數須要知道【item在舊數組的下標:previousIndex】、【剛剛講過的addRemoveOffset】、【item被多少個向前移動的項「通過」:moveOffset】,結果adjPreviousIndex
就是三者之和,它就是「item在【舊數組執行完已知操做之後的臨時數組】中的下標」。
既然咱們須要知道各個項被多少個向前移動的項「通過」,那麼咱們應該在向前移動某項的時候就記錄它通過了哪些項。好比[a,b,c,d]=>[a,d,c,b]
計算出第一個操做「d移動到1」,d向前移動的時候依次通過c,b,所以它們的moveOffset要+1;接下來計算出第二個操做「c向前移動到2」,通過b,所以b的moveOffset要再次+1。這個for循環就是作這個事情的:
for (let i = 0; i < localMovePreviousIndex; i++) { const offset = i < moveOffsets.length ? moveOffsets[i] : (moveOffsets[i] = 0); // 對於每一個可能被通過的項(舊數組第i項),計算它在臨時數組(僅僅考移動的項,不考慮增長、刪除的項)中的下標 const index = offset + i; // 判斷它是否是在臨時數組的[localMovePreviousIndex,localCurrentIndex)範圍 if (localCurrentIndex <= index && index < localMovePreviousIndex) { // 若是是,說明這一項是被「通過」的 moveOffsets[i] = offset + 1; } }
這個for循環比較難懂,這裏解釋一下:
moveOffsets
這個數組來存儲各個項的moveOffset
。這個數組以previousIndex(舊數組中的下標)爲索引。(let i = 0; i < localMovePreviousIndex; i++)
,如何理解?咱們正在檢查臨時數組(舊數組執行完已知操做之後的臨時狀態,忽略增長、刪除的項)的第localCurrentIndex
個項,此時咱們發現localMovePreviousIndex != localCurrentIndex。所以這個向要從臨時數組的localMovePreviousIndex
位置移動到localCurrentIndex
位置。所以臨時數組下標範圍[localMovePreviousIndex,localCurrentIndex)中的項都須要moveOffset+=1。爲了更新moveOffset,咱們須要知道這些項在【舊數組】中的下標。但是咱們怎麼知道這些項在【舊數組】中的下標呢?咱們沒法從【臨時下標】計算出【舊數組下標】。可是咱們可以肯定的是這些項在舊數組的下標確定小於localMovePreviousIndex
(由於這些項確定尚未被向前移動,它們只能被那些【向前移動的項】「通過」,下標只可能增長),因而咱們就對【舊數組中全部下標小於localMovePreviousIndex
的每一個項】計算它們在【臨時數組】中的下標(這就是for循環的範圍由來),而後判斷它在【臨時數組】中的下標是否處於範圍[localMovePreviousIndex,localCurrentIndex),若是是的話咱們就更新moveOffsets[i]
。總結一下這個算法的思路:
算法接受一個臨時數組和一個目標數組(最開始臨時數組是舊數組)。這個算法不斷從臨時數組構造一個新的臨時數組,使得新的臨時數組有更長的前綴匹配目標數組,直到構造出的臨時數組與目標數組徹底相同。如何構造新的臨時數組呢?將臨時數組中的某一項向前移動,移動到正確的位置。好比臨時數組是[a,d,c,b,e]
,目標數組是[a,b,c,d,e]
,咱們構造出的下一個臨時數組是[a,b,d,c,e]
(b向前移動到正確的位置),使得新的臨時數組有更長的前綴與目標數組匹配(前綴a,b
)。
繼續重複這個過程,使得新的臨時數組有更長的前綴匹配目標數組,直到構造出的臨時數組與目標數組徹底相同。
在實現的時候,Angular並無直接存儲臨時數組,而是經過一個
moveOffsets
數組,表示如何經過移動舊數組的項獲得臨時數組(這也是爲何moveOffsets是以previousIndex(舊數組中的下標)爲索引的)。
剛纔對於forEachOperation
的討論咱們常常忽略項的增長和刪除。其實增長、刪除項對其餘項的下標也有影響,道理相似,只不過此次咱們只須要用addRemoveOffset
變量記錄【到目前爲止的計算中,已經增長了多少個項】(若是刪除的項比增長的多,則這個值爲負數),而後【在經過原下標計算臨時下標的時候】加上這個值就行了。
剛纔對於forEachOperation
的討論中,咱們也沒有說明【在什麼狀況咱們要計算出一個項的增長或刪除操做】。全部要被刪除的項,在diff
執行完畢後都被放到了_removalsHead
鏈表中。誠然,咱們能夠在計算出全部移動操做以前先將刪除操做輸出,可是Angular彷佛以爲這樣不夠天然。按照咱們上面的算法,【舊數組】的每一步操做,逐漸使得【更長的舊數組前綴與新數組匹配】,而先執行全部刪除操做會破壞這種【從左往右逐一匹配】的感受。所以Angular實現的forEachOperation
,對_itHead
從左往右匹配,當匹配到被刪除項的時候,再執行刪除操做:
在遍歷_itHead
鏈表時,正在匹配的項在目標數組的下標是nextIt.currentIndex,若是nextIt.currentIndex>=【nextRemove的臨時下標】(此時這個三元表達式的值是nextRemove),就要輸出刪除nextRemove的操做(若是咱們不刪除也不移動nextRemove,此時應該輪到nextRemove被匹配了)。好比[a,b,c,d]=>[d,a,c,e]
,遍歷到_itHead
(新數組)的c
時,臨時數組爲[d,a,b,c]
,發現nextRemove(在這個例子中是b)在臨時數組中的c
以前出現(nextIt.currentIndex>=【nextRemove的臨時下標】),所以這一步不匹配c
,而先刪除b
。
以Angular的一個單元測試爲例:
[0, 1, 2, 3, 4, 5] => [6, 2, 7, 0, 4, 8]
在diff的過程當中,_itHead
和_unlinkedRecords
的變化過程以下(括號中的項是被放入_unlinkedRecords
的,加粗表示這部分_itHead
前綴已經與目標數組相匹配):
0 1 2 3 4 5 ()
6 1 2 3 4 5 (0)
6 2 3 4 5 (0 1)
6 2 7 4 5 (0 1 3)
6 2 7 0 5 (1 3 4)
6 2 7 0 4 (1 3 5)
6 2 7 0 4 8 (1 3 5)
所以diff完成之後,DefaultIterableDiffer
內部的鏈表處於以下狀態([]
表示該項下標的變化):
collection: ['6[null->0]', '2[2->1]', '7[null->2]', '0[0->3]', '4', '8[null->5]'], previous: ['0[0->3]', '1[1->null]', '2[2->1]', '3[3->null]', '4', '5[5->null]'], additions: ['6[null->0]', '7[null->2]', '8[null->5]'], moves: ['2[2->1]', '0[0->3]'], removals: ['1[1->null]', '3[3->null]', '5[5->null]']
diff完成之後就能夠經過forEachOperation
來獲取(邏輯上的)更新操做了。forEachOperation
會輸出以下更新操做,這些操做能將舊數組更新爲當前數組。(()
中表示這次操做形成的臨時下標的變化,[]
中表示這一項在就舊組中的下標,也就是item.previousIndex
)
'INSERT 6 (VOID -> 0)', 'MOVE 2 (3 -> 1) [o=2]', 'INSERT 7 (VOID -> 2)', 'REMOVE 1 (4 -> VOID) [o=1]', 'REMOVE 3 (4 -> VOID) [o=3]', 'REMOVE 5 (5 -> VOID) [o=5]', 'INSERT 8 (VOID -> 5)'
forEachOperation
的執行過程當中,構造出的臨時數組以下:
0 1 2 3 4 5
6 0 1 2 3 4 5 // 'INSERT 6 (VOID -> 0)',
6 2 0 1 3 4 5 // 'MOVE 2 (3 -> 1) [o=2]',
6 2 7 0 1 3 4 5 // 'INSERT 7 (VOID -> 2)',
6 2 7 0 1 3 4 5 // 0 不須要移動
6 2 7 0 3 4 5 // 'REMOVE 1 (4 -> VOID) [o=1]',
6 2 7 0 4 5 // 'REMOVE 3 (4 -> VOID) [o=3]',
6 2 7 0 4 5 // 4 不須要移動
6 2 7 0 4 // 'REMOVE 5 (5 -> VOID) [o=5]',
6 2 7 0 4 8 // 'INSERT 8 (VOID -> 5)'
小練習:[a,b,c,d,e]=>[a,e,f,b,d]
的diff
過程、forEachOperation
輸出是怎麼樣的?
更多範例能夠查看Angular的相關單元測試。
至此,變動算法已經介紹完了,上面的介紹忽略了一些維護鏈表的細節和邊界狀況的考慮,有興趣的讀者能夠本身閱讀一遍源代碼。