Angular源碼剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的變動檢測算法

前言

Angular原生實現了兩個工具類:DefaultKeyValueDifferDefaultIterableDiffer,它們分別用來檢查兩個對象或兩個數組之間的差異(也就是diff)。典型的使用場景是:檢查某個變量在兩個時刻之間是否發生了改變、發生了什麼樣的改變,在這篇文章中,咱們稱它爲變動檢測html

請將diff與change detection區分開來。

Angular的變化檢測默認只比較對象的引用是否改變,可是咱們能夠經過DoCheck生命週期鉤子來作一些額外的檢測,好比檢查對象是否增長刪除改動了某些屬性、檢查數組是否增長刪除移動了某些條目。這個時候變動檢測就能夠派上用場了。
舉個例子,NgForOf指令內部就是經過DefaultIterableDiffer來檢測輸入數組發生了怎樣的變化,從而可以用最小的代價去更新DOM。git

這兩個工具類中包含的算法能夠說是十分通用的,甚至能夠移植到其餘框架、語言去。除此以外,掌握這種變動檢測算法也可以幫助咱們更好地理解、使用NgForOf,甚至編寫本身的結構型指令。github

在咱們經過源碼瞭解它們的算法以前,我先簡單地介紹一下Differ是如何使用的。算法

如何在Angular中使用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供應系統」)具備很強的可擴展性框架

KeyValueDiffer

先拋開DefaultKeyValueDiffer自己不談,咱們先從源碼來看看KeyValueDiffer供應系統是如何實現的。ide

KeyValueDiffer供應系統

這個系統主要由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供應系統有如下優勢:

  1. 擴展性好,你能夠本身實現KeyValueDiffer(好比Date的變動檢測)。只要分別用class實現KeyValueDifferFactory和KeyValueDiffer接口,而後KeyValueDiffers就能夠幫助你分發你的KeyValueDifferFactory。而且,使用者經過統一的API來與Factory和Differ進行交互。
  2. KeyValueDiffers的繼承關係相似於注入器的層級關係,幫助你簡化KeyValueDiffers的建立,理清find的查找順序。
  3. 將KeyValueDiffers註冊在某個ngModule providers或者Component providers中(不要覆蓋掉ApplicationModule註冊的KeyValueDiffers!不然你沒法獲取到DeafultDiffer)。經過控制KeyValueDiffers的依賴注入有效範圍,你能夠控制你的KeyValueDiffers的分發範圍。

DefaultKeyValueDiffer

讓咱們從源碼研究它。

注意到它同時實現了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就包含了變動檢測算法:

  1. 調用reset()爲遷移到下一個狀態做準備(包括:更新_previousMapHead鏈表,更新每一個record的 _nextPrevious指針和previousValue,清空_changesHead _additionsHead _removalsHead鏈表)
  2. 遍歷新傳入的obj的每一個屬性,依次與_mapHead比較(_mapHead存儲的仍是舊obj的record)。

    1. 若是key相同,則比較value,若是value不一樣,則更新這個record並將它加入_changesHead 鏈表。
    2. 若是key都不相同,那麼有可能這個key在鏈表的後面。所以在_getOrCreateRecordForKey方法中,先嘗試從_records Map找到這個key,若是找到了就比較其value是否與新obj中的value相同(若是不一樣的話就_addToChanges),而後將它暫時從_mapHead鏈表中刪除_getOrCreateRecordForKey返回這個record(等一下會插入到鏈表的正確位置); 若是在Map找不到這個key,說明這是一個新加入的屬性,則建立一個新的record並加入_additionsHead鏈表,_getOrCreateRecordForKey返回這個新建的record(等一下再插入到_mapHead鏈表中)。_getOrCreateRecordForKey執行完畢之後,將返回的record插入_mapHead鏈表的正確位置
  3. 新傳入的obj的每一個屬性都遍歷過之後,若是_mapHead鏈表中還有還沒有訪問的record,這些record都是被刪除的。將它們從_mapHead移除、加入_removalsHead、從_records中刪除這些條目、更新這些record的狀態。

實現這個算法時要理清楚何時更新_changesHead _additionsHead _removalsHead鏈表,也就是什麼狀況意味着發現了change、addition、removal。這在上面的表述中已經說明了。
理清楚了這一點之後,剩下的就是維護鏈表的操做了。同時維護這麼多的鏈表確實是一件很容易出錯的事情。

IterableDiffer

IterableDiffer用來對數組或類數組對象進行變動檢測。

IterableDiffer供應系統

IterableDiffer供應系統與KeyValueDiffer供應系統很是相似,這裏只討論幾個比較重要的地方:

  1. IterableChanges.forEachOperation可讓用戶知道,這個數組的上次變動中作了哪些操做。也就是說從舊arr經歷哪些增長、刪除、移動可以變成新arr。注意這些操做不必定是實際發生在舊arr上的,畢竟有不止一種操做可以將舊arr變成新arr。
  2. IterableDiffer經過trackByFn來肯定新arr中的某個項與舊arr中某個項是否相同。而剛纔的DefaultKeyValueDiffer是直接經過looseIdentical來判斷新舊value是否相同的(大體等同於===判斷)。
  3. IterableChanges.forEachIdentityChange可讓用戶看到全部trackById相同但Identity變化(至關於a!==b)的那些條目。

DefaultIterableDiffer

那些簡單的,或者DefaultKeyValueDiffer也有的類成員我就不一一介紹了。

與前面相似地,變動檢測的邏輯封裝在_check函數中。讓咱們從這裏開始。

  1. this._reset()進行初步的狀態更新。包括:更新_previousItHead鏈表,更新每一個record的 _nextPrevious指針,重置previousIndex,清空_additionsHead _movesHead _removalsHead _identityChangesHead鏈表。
  2. 判斷Array.isArray(collection),因爲DefaultIterableDiffer支持一些類數組對象,所以在判斷不成功的時候會執行另外一種算法來檢測變動。咱們不妨假設檢測正常數組的變動。
  3. 對於collection(新數組)的每一個項,執行如下操做(用下標index來遍歷collection):

    1. this._trackByFn(index, item)計算當前項的標識值。**若是新舊數組之間的兩個項的標識值相等,咱們就認爲它們是同一個項,無論identity是否同樣(即無論a===b是否成立),咱們都認爲順序沒有變化。
    2. 比較_itHead鏈表(舊數組)的第index個項(命名爲item1)與collection的第index個項(命名爲item2),它們標識值是否相同:

      • 若是不相同,調用_mismatch來處理,使得item2成爲_itHead鏈表的第index個項:

        1. 先將「item1」從_itHead鏈表中刪除,畢竟它們沒有在正確的位置上(若是後面發現有這個項,再將它加到合適的位置)。而後將它加入_unlinkedRecords中(它是_DuplicateMap類型,也就是MuitiMap的一種實現。之因此要用到MuitiMap,是由於數組中可能有多個項的標識值相同),而後將它加入_removalsHead鏈表中。
        2. 嘗試在舊數組的index之後的項中找到有相同標識值的項。若是找到的話,就檢測到移動變動,因而要將這個項從_itHead鏈表中原來的位置移動到index位置,並加入_movesHead鏈表。若是沒有在舊數組找到相同標識值的項,嘗試從_unlinkedRecords找到相同標識值的項。若是找到的話,一樣檢測到移動變動。將這個項從_unlinkedRecordsMap和_removalsHead鏈表移除(撤銷_addToRemovals操做),而後插入到_itHead鏈表的index位置。若是從_unlinkedRecords仍是沒找到相同標識值的項,說明這是一個新增長的項,因而將它插入_itHead鏈表的index位置並加入_additionsHead鏈表。
        • _mismatch執行完畢之後。設置mayBeDirty = true。這個標識表示未來每次檢測到item1item2標識值相等得時候,要調用_verifyReinsertion來修正某種錯誤,下面再談。
      • 若是標識值相同,且mayBeDirty==true,須要調用_verifyReinsertion來檢查前面步驟可能產生的插入順序錯誤:假設發生變動[a, a] => [b, a, a],那麼在對比鏈表中的a和新數組的b之後,會刪除鏈表中的a(鏈表存儲的是舊數組),而後插入新數組的b,接下來,鏈表中的下一個項依然是a,就會匹配新數組中的第一個a(舊數組的第二個a匹配新數組的第一個a),接下來會在鏈表的末尾reinsert剛纔刪除的a(原數組的第一個a)。通過這樣的變動檢測之後,兩個a的順序變了正確的作法應該是「將b插入數組0位置」,而不是「將數組0位置的a換成b,而後在數組末尾加入a」。
        Angular糾正這種錯誤的方法是:每次檢測到item1(舊數組項)與item2(新數組項)標識值相等得時候,若是mayBeDirty==true,不立刻認定item2就對應於原數組的item1,而是先檢查以前是否刪除過相同標識值的項(檢查_unlinkedRecords中是否有相同標識值的項),若是有刪除過,則這個項才與item2對應,因而撤銷被刪除項的_addToRemovals操做,並將這個項reinsert到鏈表的index位置。
        _verifyReinsertion還有另外一個做用,你那就是檢查record.currentIndex是否正確。假如在record前面已經插入一個項並刪除一個項,那麼currentIndex不須要改變;可是若是隻是前面插入了1個,那麼插入項之後的全部項的currentIndex都要+1,而後記錄這個移動操做(_addToMoves)。在這種狀況下,雖說「這些項都移動了」不太準確,可是畢竟它們所在的下標都變化了,咱們仍是先記錄這些移動,之後調用forEachOperation的時候會過濾掉這種不嚴格的移動。
  4. 若是新數組的全部項都遍歷完了,_itHead鏈表後面還有沒訪問到的項,則這些項是被刪除的使用_truncate從鏈表中刪掉它們,並記錄它們的刪除(_addToRemovals)。_truncate除此以外還作一些收尾工做:將檢測變動時用來查詢的_unlinkedRecordsMap清空(這些是被刪除的項,它們已經被執行_addToRemovals了),而後將各類鏈表尾的next賦值爲null(咱們以前加入鏈表的時候都沒有考慮它是否是鏈表尾)。

能夠看出,算法的重點在於第3步的for循環。for循環剛開始的時候,_itHead鏈表仍是舊數組的狀態。而後通過一輪循環,就修改_itHead鏈表,將正確的項移動到_itHead鏈表的index位置。所以,這個for循環從左向右逐項更新_itHead鏈表,使得它有愈來愈長的前綴與新數組匹配。

DefaultIterableDiffer.forEachOperation

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,依次檢查其臨時下標和目標下標(在源碼中分別命名爲adjPreviousIndexcurrentIndex)。臨時下標的意思是,舊數組剛執行完已計算出的操做所獲得的臨時狀態中,這個項的下標。目標下標的意思是,這個項在最終目標數組中的下標。好比,計算[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的狀況。

其實adjPreviousIndexcurrentIndex分別表示【不忽略增長、刪除項狀況下的】臨時下標和目標下標。經過如下兩個減法,能計算出【忽略增長、刪除項的狀況下的——也就是說假設被增長、刪除的項歷來都不存在】臨時下標和目標下標:

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循環比較難懂,這裏解釋一下:

  1. Angular使用moveOffsets這個數組來存儲各個項的moveOffset。這個數組以previousIndex(舊數組中的下標)爲索引
  2. for循環的範圍是(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的相關單元測試


至此,變動算法已經介紹完了,上面的介紹忽略了一些維護鏈表的細節和邊界狀況的考慮,有興趣的讀者能夠本身閱讀一遍源代碼。

相關文章
相關標籤/搜索