中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React?DOM-diff節點源碼分析

發布時間:2023-02-27 11:49:04 來源:億速云 閱讀:97 作者:iii 欄目:開發技術

本篇內容介紹了“React DOM-diff節點源碼分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

單節點

單節點的dom-diff是在reconcileSingleElement中進行的,而能否復用的判斷依據就是將要更新的虛擬DOM的key和HTML元素的類型(即divp的區別)是否和當前(頁面上正在渲染的)真實DOM的fiber一致。

React?DOM-diff節點源碼分析

如圖所示,對于單節點的diff我們按照圖中的流程,結合源碼進行一一解讀

/**
   * 
   * @param {*} returnFiber 根fiber div#root對應的fiber
   * @param {*} currentFirstChild 老的FunctionComponent對應的fiber
   * @param {*} element 新的虛擬DOM對象
   * @returns 返回新的第一個子fiber
   */
  function reconcileSingleElement(returnFiber, currentFirstChild, element) {
    //新的虛擬DOM的key,也就是唯一標準
    const key = element.key;        // null
    let child = currentFirstChild; //老的FunctionComponent對應的fiber
    while (child !== null) {
      //判斷此老fiber對應的key和新的虛擬DOM對象的key是否一樣 null===null
      if (child.key === key) {
        //判斷老fiber對應的類型和新虛擬DOM元素對應的類型是否相同
        if (child.type === element.type) {// p div
          deleteRemainingChildren(returnFiber, child.sibling);
          //如果key一樣,類型也一樣,則認為此節點可以復用
          const existing = useFiber(child, element.props);
          existing.ref = element.ref;
          existing.return = returnFiber;
          return existing;
        } else {
          //如果找到一key一樣老fiber,但是類型不一樣,不能此老fiber,把剩下的全部刪除
          deleteRemainingChildren(returnFiber, child);
        }
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
    //因為我們現實的初次掛載,老節點currentFirstChild肯定是沒有的,所以可以直接根據虛擬DOM創建新的Fiber節點
    const created = createFiberFromElement(element);
    created.ref = element.ref;
    created.return = returnFiber;
    return created;
  }

key相同,類型相同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 變化到 -->
<div>
  <div key='A'>C</div>
</div>

對于上面列舉到的情況,新的虛擬DOM匹配到第一個即為相同key和type,我們首先通過deleteRemainingChildren方法刪除掉其它的多余的子節點(上面的 <div key='B'>B</div>),然后通過useFiber方法來復用老fiber產生新的fiber,這樣就完成我們的復用。

key不同,類型相同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 變化到 -->
<div>
  <div key='C'>C</div>
</div>

對于上面列舉到的情況,新的虛擬DOM匹配到第一個即為不同key即使type相同也不會往下進行,通過deleteChild方法刪掉第一個子節點,即<div key='A'>A</div>對應的fiber,然后再對第二個子節點<div key='B'>B</div>進行對比,發現key依然不同,繼續刪除,刪除完成之后child === null成立,跳出while循環,通過createFiberFromElement方法根據新的虛擬DOM創建新的fiber。

key相同,類型不同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 變化到 -->
<div>
  <p key='A'>C</p>
</div>

對于上面列舉的情況,第一次匹配到了相同的key但是type不同,依舊是不符合復用的條件,而且此時會通過deleteRemainingChildren方法刪除掉所有子節點,即不會再進行第二次比較,直接就跳出循環,通過createFiberFromElement方法根據新的虛擬DOM創建新的fiber。

多節點

多節點的diff相對于單節點的diff來說更加復雜一些。這里主要是在方法reconcileChildrenArray中進行,這個過程最多會經歷三次遍歷,每次完成相應的功能,下面我們結合源碼來具體探究一下。

  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
    let resultingFirstChild = null; //返回的第一個新兒子
    let previousNewFiber = null; //上一個的一個新的兒fiber
    let newIdx = 0;//用來遍歷新的虛擬DOM的索引
    let oldFiber = currentFirstChild;//第一個老fiber
    let nextOldFiber = null;//下一個第fiber
    let lastPlacedIndex = 0;//上一個不需要移動的老節點的索引
    // 開始第一輪循環 如果老fiber有值,新的虛擬DOM也有值
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      //先暫下一個老fiber
      nextOldFiber = oldFiber.sibling;
      //試圖更新或者試圖復用老的fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (newFiber === null) {
        break;
      }
      //如果有老fiber,但是新的fiber并沒有成功復用老fiber和老的真實DOM,那就刪除老fiber,在提交階段會刪除真實DOM
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
      //指定新fiber的位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber
    }
    //新的虛擬DOM已經循環完畢
    if (newIdx === newChildren.length) {
      //刪除剩下的老fiber
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    if (oldFiber === null) {
      //如果老的 fiber已經沒有了, 新的虛擬DOM還有,進入插入新節點的邏輯
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx]);
        if (newFiber === null) continue;
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        //如果previousNewFiber為null,說明這是第一個fiber
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //這個newFiber就是大兒子
        } else {
          //否則說明不是大兒子,就把這個newFiber添加上一個子節點后面
          previousNewFiber.sibling = newFiber;
        }
        //讓newFiber成為最后一個或者說上一個子fiber
        previousNewFiber = newFiber;
      }
    }
    // 開始處理移動的情況
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //開始遍歷剩下的虛擬DOM子節點
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
      if (newFiber !== null) {
      //如果要跟蹤副作用,并且有老fiber
       if (newFiber.alternate !== null) {
         existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
       }
        //指定新的fiber存放位置 ,并且給lastPlacedIndex賦值
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //這個newFiber就是大兒子
        } else {
          //否則說明不是大兒子,就把這個newFiber添加上一個子節點后面
          previousNewFiber.sibling = newFiber;
        }
        //讓newFiber成為最后一個或者說上一個子fiber
        previousNewFiber = newFiber;
      }
    }
    //等全部處理完后,刪除map中所有剩下的老fiber
    existingChildren.forEach(child => deleteChild(returnFiber, child));
    return resultingFirstChild;
  }

這段代碼是比較長的,這里全部貼出來就是體現其完整性。下面幫助大家逐步的分析。

<ul key="container">
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
  <li key="D">D</li>
  <li key="E">E</li>
  <li key="F">F</li>
</ul>
<!-- 變化到 -->
<ul key="container">
  <li key="A">A2</li>
  <li key="C">C2</li>
  <li key="E">E2</li>
  <li key="B">B2</li>
  <li key="G">G</li>
  <li key="D">D2</li>
</ul>

第一次遍歷

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      //先暫下一個老fiber
      nextOldFiber = oldFiber.sibling;
      //試圖更新或者試圖復用老的fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (newFiber === null) {
        break;
      }
      if (shouldTrackSideEffects) {
        //如果有老fiber,但是新的fiber并沒有成功復用老fiber和老的真實DOM,那就刪除老fiber,在提交階段會刪除真實DOM
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      //指定新fiber的位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber
    }

我們所有的對比都是基于新節點的虛擬DOM和老節點的fiber,當我們對比A1和A2時,會根據updateSlot方法進行條件判斷,發現他們的key和type相同,符合復用條件返回創建好的fiber,我們的操作指針都指向下一個操作節點,開始對下一個節點進行第一次遍歷。

當我們對比C2和B時,因為C2和B的key并不相同,updateSlot返回null,第一次遍歷break開始進入第二次遍歷。

第二次遍歷

if (oldFiber === null) {
  //如果老的 fiber已經沒有了, 新的虛擬DOM還有,進入插入新節點的邏輯
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx]);
    if (newFiber === null) continue;
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    //如果previousNewFiber為null,說明這是第一個fiber
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber; //這個newFiber就是大兒子
    } else {
      //否則說明不是大兒子,就把這個newFiber添加上一個子節點后面
      previousNewFiber.sibling = newFiber;
    }
    //讓newFiber成為最后一個或者說上一個子fiber
    previousNewFiber = newFiber;
  }
}

然而oldFiber依舊是存在的,會直接進入到第三次遍歷,但是我們這里帶大家梳理一下,看看是如何操作的。這里的遍歷主要是針對新節點還存在,但是老fiber已經沒有了,即新更新的節點要多余老節點的情況,我們這里需要做的就是將剩下的新節點的fiber通過createChild創造出來。

第三次遍歷

// 開始處理移動的情況
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//開始遍歷剩下的虛擬DOM子節點
for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
  );
  if (newFiber !== null) {
  //如果要跟蹤副作用,并且有老fiber
  if (newFiber.alternate !== null) {
    existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
  }
    //指定新的fiber存放位置 ,并且給lastPlacedIndex賦值
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber; //這個newFiber就是大兒子
    } else {
      //否則說明不是大兒子,就把這個newFiber添加上一個子節點后面
      previousNewFiber.sibling = newFiber;
    }
    //讓newFiber成為最后一個或者說上一個子fiber
    previousNewFiber = newFiber;
  }
}
function mapRemainingChildren(returnFiber, currentFirstChild) {
  const existingChildren = new Map();
  let existingChild = currentFirstChild;
  while (existingChild != null) {
    //如果有key用key,如果沒有key使用索引
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

接下來我們進行第三次遍歷,也就是我們節點移動的情況,這里的復用是比較復雜了。

首先我們會創造一個Map來承接所有的剩余的老節點,接下來我們會根據key,或者index,來挑選老節點以供復用。找到一個能復用的節點,就會在Map中刪除對應的節點,如果有對應的點就復用,沒有就新創建節點。

React?DOM-diff節點源碼分析

  • 多個節點數量不同、key 不同;

  • 第一輪比較 A 和 A,相同可以復用,更新,然后比較 B 和 C,key 不同直接跳出第一個循環;

  • 把剩下 oldFiber 的放入 existingChildren 這個 map 中;

  • 然后聲明一個lastPlacedIndex變量,表示不需要移動的老節點的索引;

  • 繼續循環剩下的虛擬 DOM 節點;

  • 如果能在 map 中找到相同 key 相同 type 的節點則可以復用老 fiber,并把此老 fiber 從 map 中刪除;

  • 如果能在 map 中找不到相同 key 相同 type 的節點則創建新的 fiber;

  • 如果是復用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果是要移動老 fiber,不變;

  • 如果是復用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果否則更新 lastPlacedIndex 為老 fiber 的 index;

  • 把所有的 map 中剩下的 fiber 全部標記為刪除;

  • (刪除#li#F)=>(添加#li#B)=>(添加#li#G)=>(添加#li#D)=>null。

“React DOM-diff節點源碼分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

德惠市| 新蔡县| 墨脱县| 汝城县| 景洪市| 太谷县| 长汀县| 都兰县| 木兰县| 克什克腾旗| 达日县| 永川市| 名山县| 全州县| 封丘县| 获嘉县| 南开区| 北海市| 克拉玛依市| 伊宁市| 京山县| 称多县| 饶阳县| 宁都县| 黎城县| 襄垣县| 开鲁县| 临澧县| 萨迦县| 灵丘县| 阿图什市| 南溪县| 镇原县| 滨海县| 松溪县| 永修县| 中方县| 济阳县| 博爱县| 阿鲁科尔沁旗| 万州区|