虛擬DOM與DIFF算法學習

1、舊社會的頁面渲染

       在jQuery橫行的時代,FEer們,經過各類的方式去對頁面的DOM進行操做,計算大小,變化,來讓頁面生動活潑起來,豐富的DOM操做,讓一個表面簡單的頁面能展現出花通常的操做。javascript

       這個時候,人們經過DOM簡單的方法去對頁面DOM結構做出操做和改變,每一次數據的革新,都經過複雜的操做去執行變化。直到有人指出,瀏覽器對於頁面的paintlayout是有性能消耗的,屢次layout會佔用大量的瀏覽器計算內存。java

       因而人們開始想辦法去減小layout,儘量的去利用class改變,fragment包裹,display的設置來操做dom,避免瀏覽器的layout發生。甚至還有人根據瀏覽器渲染的頻率,經過requestAnimationFrame的回調來觸發渲染。react

       然而,隨着三大框架的誕生,有一羣人站出來呼籲你們放棄掉對dom的操做,用狀態去控制組件的生命,用狀態去控制頁面的變化,而與dom打交道的事,交給他們來作就好了……web

2、React的DOM模擬

       每個頁面都是由DOM節點構成的,這能夠從DOM的原意Document Object Model 文檔對象模型看出來,頁面文檔都是經過DOM節點組成,這是HTML的基架。所以而言,想要頁面是可動態變化的,就不得不去對頁面作一些操做。fragmentrequestAnimationFrame 的使用,告訴了咱們,頁面的操做其實能夠經過一個異步的形式來完成,將屢次操做合併在一次當中執行,根據必定的週期去從新改變頁面的顯示。
       可是很大的一個痛點就是,頁面中存在多個地方的內容須要變化,那我在最後的變化關頭應該如何取捨對dom的操做呢?我如何知道,哪些須要變化,哪些不須要變化呢?算法

       react的開發者明確了一個首要的問題,那就是如何衡量dom何時須要變化什麼位置須要變化segmentfault

       衆所周知,每個DOM節點都帶有自身的屬性,一個簡單的div上面掛載了幾十個attributes,顯然dom其實能夠看做一個對象,這是就是虛擬dom的原型——以對象的形式去模擬dom,這樣子操做先後的dom就能夠量化他們的區別,dom的比較就能被開發者握在手中。經過每次操做去變化dom上各對象,子對象的屬性,最後統一與現有dom結構作對比,就能計算出當中的差別,最後去對這些差別進行dom操做,就能夠簡單的對頁面進行變動。瀏覽器

       虛擬dom(VirtualDOM)由此產生,它是平行於dom的另外一套對象體系,用於記錄dom的狀態,我上一篇文章setState的介紹,其實就是對虛擬dom屬性的變化操做,其中的batching就是虛擬dom最核心的阻塞功能,經過事務去控制數據在一個週期內不作屢次更新,經過state去保證最後的狀態即爲要更新的狀態。框架

       有了虛擬dom,對頁面元素進行了抽象,其實才是虛擬dom最重要的意義,這種概念讓react有了’一次編寫,到處運行’的堅實基礎。
       既然知道虛擬dom的實際是帶有屬性的對象,那麼咱們解決了第一個問題,也就是如何衡量dom何時須要變化,接下來就介紹一下如何衡量,什麼位置須要變化?dom

3、react的diff算法與三大策略

       虛擬dom和dom有一個共同的特色,就是樹形結構。比較虛擬dom與dom的差別,以及對dom節點的操做,其實就是樹的差別比較,就是對樹的節點進行替換。對於樹的比較,有一個最簡單的方法就是循環遞歸比較樹的節點,也就是傳統的diff算法,這個算法的複雜度達到了立方級別,效率能夠說不好。
       而react使用diff算法的時候,根據應用場景大膽的改變了算法自己的難度,硬生生把算法複雜度降到了O(n)異步

       react diff遵循三個策略:

  1. webUI種,dom節點跨層級移動操做特別少,因此忽略這種操做
  2. 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構
  3. 對於同一層級的一組子節點,它們能夠經過惟一id進行區分

       這麼聽下來,感受這三個策略莫名奇妙,接下來,我經過diff算法在不一樣層次的比較,介紹diff算法如何去利用這三個策略高效更新組件的

4、tree diff與跨層級dom操做

       所謂的跨層級dom操做是指,本來在dom節點A下面的一個組件a,被移動到dom節點B下面。這種操做我見過最多見的應用場景是把左側區域的數據添加到右側區域,然而除了這種業務場景以外,不多有把一個dom從自身的容器中移到另外一個容器中的狀況,所以,策略一,這種操做在react當中默認是不多有的操做。

       忽略掉這種特殊的狀況後,react大膽的修改了diff算法——按直系兄弟節點比較比較。多個組件擁有同一個父組件,歸爲一個組,整組與變化後的狀況作對比,缺了就補上,多了就刪除。一層一層的,從上至下進行按層的比較。這種比較過程當中,若是發現某一個節點發生變化,能夠直接不遍歷其子節點,直接統一刪除整個節點以及其子節點,把新節點直接替換上去,這樣子大大減小了遍歷的消耗。

clipboard.png

       基於這種’粗暴的方式’來修改頁面dom,若是是進行跨層級操做,則會刪除一個完整節點,再新增一個完整節點,這是高消耗的事情,因此react考慮到跨層級操做不多,因此做出了react diff的優化。

5、componet diff

react當中,組件的概念貫通全局,因此到了組件新舊的比較上,react直接給出策略:

  1. 若是是同一類型組件,按照原策略繼續比較Virtual DOM樹便可。
  2. 若是不是同一類型組件,則將該組件斷定爲dirty component,從而替換整個組件下全部子節點。
  3. 對於同一類型的組件,有可能其Virtual DOM沒有任何變化,若是能確切知道這點,那麼能夠節省大量diff運算時間。所以,react容許經過shouldComponentUpdate()斷定該組件是否須要進行diff分析。

       一、3兩條對於react的使用者來講很是好理解,同樣的組件,直接比較子組件的變化便可;開發者能夠控制組件更新的條件,也能夠對diff進行優化。

       那麼第2條可能就不是那麼容易被理解了,爲何不是同一類型的組件就必定直接替換呢?好比我一個A組件一個B組件,都是一個div嵌套一個p標籤,裏面有個span包裹的文字,兩個組件只有文字不一樣,那我直接替換組件豈不是浪費了divpspan標籤的從新建立嗎?

       這裏react認爲,兩個不一樣類型的組件,在實際處理業務過程當中,結構一致的機率很低。由於既然拆分爲兩個不一樣的組件,若裏面的dom結構還很是類似,只能說明對組件的拆分粒度還不夠。不一樣組件在業務上完成的事情應該是不一樣的,因此不去考慮結構近似的狀況,直接替換組件。

6、element diff

       咱們介紹tree diff的時候有提到過,react在對比區別的時候是經過同一個父組件下的全部兄弟節點爲一組進行新老對比的。這當中對比的細節纔是整個diff算法最精粹的地方。
       react對待同一層級的代碼只會進行三種操做,插入刪除移動。咱們能夠理解爲本來沒有的組件,咱們進行插入操做;新的變化以後消失的組件咱們進行刪除操做;一個組件新舊變化以後,位置發生偏移,則使用移動操做。
       具體的移動方案,你們能夠看看大神的這篇知乎,裏面講的更多例子https://zhuanlan.zhihu.com/p/...
       而我直接在react這段的代碼中增長註釋,帶你們一塊兒看看react diff的詳細邏輯

{
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
  var prevChildren = this._renderedChildren; // 變動前的現有dom
  var nextChildren = this._reconcilerUpdateChildren(
    prevChildren, nextNestedChildrenElements, transaction, context
  ); // 待更新dom
  if (!nextChildren && !prevChildren) { // 兩者有一個不存在,則退出方法
    return;
  }
  var name;
  var lastIndex = 0; // 這個值很是關鍵,用來決定移動位置
  var nextIndex = 0; // 節點指針,填充一個位置向後移一位
  for (name in nextChildren) { // 開始遍歷新節點,與老節點作對比,這裏的name能夠理解爲組件的key,惟一標誌
    if (!nextChildren.hasOwnProperty(name)) {
      continue; // porto上的屬性不要
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    if (prevChild === nextChild) { //看看老節點上面有沒有相同的節點,若是存在就再也不新建了
      // 移動節點
      this.moveChild(prevChild, nextIndex, lastIndex);
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);//新的移動下標,移動的時候老節點和移動下標,哪一個大取哪一個
      prevChild._mountIndex = nextIndex; // 老節點下標直接移動到現有下標位置
    } else { // 老集合裏面有相同名稱節點,可是內容不一樣了,直接把老的刪了,新建一個
      if (prevChild) {
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 刪除節點
        this._unmountChild(prevChild);
      }
      // 初始化並建立節點
      this._mountChildAtIndex(
        nextChild, nextIndex, transaction, context
      );
    }
    nextIndex++;
  }
  for (name in prevChildren) { // 所有都遍歷完了,老節點中剩餘的節點刪除掉
    if (prevChildren.hasOwnProperty(name) &&
        !(nextChildren && nextChildren.hasOwnProperty(name))) {
      this._unmountChild(prevChildren[name]);
    }
  }
  this._renderedChildren = nextChildren; // 新節點完成
},
// 移動節點
moveChild: function(child, toIndex, lastIndex) {
  if (child._mountIndex < lastIndex) { // 若是當前新節點下標小於已移動下標,則不用移動
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
  }
},
// 建立節點
createChild: function(child, mountImage) {
  this.prepareToManageChildren();
  enqueueInsertMarkup(this, mountImage, child._mountIndex);
},
// 刪除節點
removeChild: function(child) {
  this.prepareToManageChildren();
  enqueueRemove(this, child._mountIndex);
},

_unmountChild: function(child) {
  this.removeChild(child);
  child._mountIndex = null;
},

_mountChildAtIndex: function(
  child,
  index,
  transaction,
  context) {
  var mountImage = ReactReconciler.mountComponent(
    child,
    transaction,
    this,
    this._nativeContainerInfo,
    context
  );
  child._mountIndex = index;
  this.createChild(child, mountImage);
},
}

       就此,react虛擬dom和diff算法就介紹完了,讀懂上面的代碼,至少要知道,react中爲何儘可能減小一個dom從最後移動到最前面的操做,就算是理解了。

相關文章
相關標籤/搜索