React Dom-diff之多节点

react中DOM DIFF的三个规则

  • 只对同级元素进行比较,不同层级不比较
  • 不同的类型对应不同的元素
  • 可以通过key来标识同一个节点

react中DOM DIFF的遍历规则

  • 第一轮

    • 如果key不同,则直接结束本轮循环
    • newChildren和oldFiber遍历完,结束本轮循环
    • key相同而type不同,标记老的oldFiber为删除,继续循环
    • key相同type相同,则可以复用老节点(oldFiber),继续循环
  • 第二轮

    • newChildren遍历完而oldFiber还有,遍历剩下的所有oldFiber标记为删除,DIFF结束
    • oldFiber遍历完了,而newChildren还有,将剩下的newChildren标记为插入,DIFF结束
    • newChildren和oldFiber都同时遍历完,DIFF结束
    • newChildren和oldFiber都没有完成,则进行节点移动的逻辑
  • 第三轮

    • 处理节点移动的情况

多个节点的数量和key相同,有的type不同

如上图所示,多个节点的数量和key相同,有的type不同。

/**
 * 如果新的虚拟DOM是一个数组的话, 也就是说有多个儿子的话
 * @param {*} returnFiber ui
 * @param {*} currentFirstChild null
 * @param {*} newChild [liA,liB,liC]
 */
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 如果没有老fiber,也就是初次挂载的时候
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

这个是之前reconcileChildrenArray处理没有oldFiber的情况,现在做其他情况的处理

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  
  // +++++++++++++++++++++++++++++++++++++++++++++++++
  
  //下一个老fiber
  let nextOldFiber = null;
  //处理更新的情况 老fiber和新fiber都存在
    for (; oldFiber && newIdx < newChildren.length; newIdx++) {
      //先缓存下一个老fiber
      nextOldFiber = oldFiber.sibling;
      //试图复用老fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      //如果key 不一样,直接跳出第一轮循环
      if (!newFiber)
        break; // 跳出第一轮循环
      //老fiber存在,但是新的fiber并没有复用老fiber
      if (oldFiber && !newFiber.alternate) {
        deleteChild(returnFiber, oldFiber);
      }
      //核心是给当前的newFiber添加一个副作用flags 叫新增
      placeChild(newFiber, lastPlacedIndex, newIdx);
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
  
  // +++++++++++++++++++++++++++++++++++++++++++++++++
  
  // 如果没有老fiber,也就是初次挂载的时候
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

updateSlot试图复用老fiber

function updateSlot(returnFiber, oldFiber, newChild) {
  const key = oldFiber ? oldFiber.key : null;
  //如果新的虚拟DOM的key和老fiber的key一样
  if (newChild.key === key) {
    return updateElement(returnFiber, oldFiber, newChild);
  } else {
    //如果key不一样,直接结束返回null
    return null;
  }
}

更新元素

function updateElement(returnFiber, oldFiber, newChild) {
  if (oldFiber) {
    if (oldFiber.type === newChild.type) {
      const existing = useFiber(oldFiber, newChild.props);
      existing.return = returnFiber;
      return existing;
    }
  }
  //如果没有老fiber
  const created = createFiberFromElement(newChild);
  created.return = returnFiber;
  return created;
}
function placeChild(newFiber, lastPlacedIndex, newIdx) {
  newFiber.index = newIdx;
  if (!shouldTrackSideEffects) {
    return;
  }
  const current = newFiber.alternate;
  //如果有current说是更新,复用老节点的更新,不会添加Placement
  if (current) {
    // TODO
  } else {
    newFiber.flags = Placement;
  }
}

目前完整的reconcileChildrenArray代码逻辑如下,加了老fiber和新fiber都存在的处理逻辑,按照li(A),li(B),li©到li(A),p(B),li©的更新逻辑,li(A)通过updateSlot的处理,是可以复用的,updateElement就是处理更新或者创建fiber;li(B)的时候updateSlot返回了新创建的p(B),并且需要删除li(B),oldFiber && !newFiber.alternate这种情况就符合了li(B)的情况,placeChild就是把p(B)标记为插入;li©的情况和li(A)一样。

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    placeChild(newFiber,newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  // 如果没有老fiber,也就是初次挂载的时候
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

来简单测试下:

<div>
  <button id="multi1">5.多个节点的数量和key相同,有的type不同</button><br/>
  &lt;ul key=&quot;ul&quot;&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;B&quot;&gt;B&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;C&quot;id=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
  &lt;/ul&gt;<br/>
  <button id="multi1Update">更新属性,type不同的删除老节点,删除新节点</button><br/>
  &lt;ul key=&quot;ul&quot;&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;p key=&quot;B&quot; id=&quot;B2&quot;&gt;B2&lt;/p&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;C&quot; id=&quot;C2&quot; &gt;C2&lt;/li&gt;<br/>
  &lt;/ul&gt;<br/>
</div>
<hr/>
multi1.addEventListener('click', () => {
  let element = (
    <ul key="ul">
      <li key="A">A</li>
      <li key="B" id="B">B</li>
      <li key="C" id="C">C</li>
    </ul>
  );
  ReactDOM.render(element, root);
});
multi1Update.addEventListener('click', () => {
  let element = (
    <ul key="ul">
      <li key="A">A</li>
      <p key="B" id="B2">B2</p>
      <li key="C" id="C2">C2</li>
    </ul>
  );
  ReactDOM.render(element, root);
});

可以看到搜集的effectList是正确的,但实际上DOM插入的顺序不太对,是因为在之前提交的时候,也就是commitWork阶段commitPlacement插入节点的时候有问题,看原来代码如下:

export function commitPlacement(nextEffect) {
  let stateNode = nextEffect.stateNode;
  let parentStateNode = getParentStateNode(nextEffect);
  appendChild(parentStateNode, stateNode);
}

问题就是出在appendChild的这个时候,做下调整:

export function commitPlacement(nextEffect) {
  let stateNode = nextEffect.stateNode;
  let parentStateNode = getParentStateNode(nextEffect);
  appendChild(parentStateNode, stateNode);
  let before = getHostSibling(nextEffect);
  if (before) {
    insertBefore(parentStateNode, stateNode, before);
  } else {
    appendChild(parentStateNode, stateNode);
  }
}
//当前fiber后面一个离它最近的真实的DOM节点
function getHostSibling(fiber) {
  let node = fiber.sibling;
  while (node) {
    //找它的弟弟们,找到最近一个并且不是插入的节点,返回。。没有更新,更新
    if (!( node.flags & Placement )) {
      return node.stateNode;
    }
    node = node.sibling;
  }
  return null;
}
export function insertBefore(parentInstance, child, before) {
  parentInstance.insertBefore(child, before);
}

再来测试下:

可以看到effectList和DOM都是正确的。

多个节点的类型和key全部相同,有新增元素

<div>
  <button id="multi2">6.多个节点的类型和key全部相同,有新增元素</button><br/>
  &lt;ul key=&quot;ul&quot;&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;B&quot;&gt;B&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
  &lt;/ul&gt;<br/>
  <button id="multi2Update">增加新元素并更新老元素</button><br/>
  &lt;ul key=&quot;ul&quot;&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;B2&quot;&gt;B2&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
  &nbsp;&nbsp;&lt;li key=&quot;D&quot;&gt;D&lt;/li&gt;<br/>
  &lt;/ul&gt;<br/>
</div>
<hr/>
multi2.addEventListener('click', () => {
  let element = (
    <ul key="ul">
      <li key="A">A</li>
      <li key="B" id="B">B</li>
      <li key="C">C</li>
    </ul>
  );
  ReactDOM.render(element, root);
});
//增加新元素并更新老元素
multi2Update.addEventListener('click', () => {
  let element = (
    <ul key="ul">
      <li key="A">A</li>
      <li key="B" id="B2">B2</li>
      <li key="C">C</li>
      <li key="D">D</li>
    </ul>
  );
  ReactDOM.render(element, root);
});

对于这种情况,就说明oldFiber没有了,新的fiber还有,就会走到if(!oldFiber)的分支去,在里面放置下新fiber就可以了,具体代码就是加一行placeChild(newFiber, newIdx)。

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    debugger
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    placeChild(newFiber,newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      placeChild(newFiber, newIdx) 
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

可以看到是更新了B,插入新的D。

多个节点的类型和key全部相同,有删除老元素

<div>
   <button id="multi3">7.多个节点的类型和key全部相同,有删除老元素</button><br/>
   &lt;ul key=&quot;ul&quot;&gt;<br/>
     &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
     &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;B&quot;&gt;B&lt;/li&gt;<br/>
     &nbsp;&nbsp;&lt;li key=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
   &lt;/ul&gt;<br/>
   <button id="multi3Update">删除老元素并更新老元素</button><br/>
   &lt;ul key=&quot;ul&quot;&gt;<br/>
     &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
     &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;B2&quot;&gt;B2&lt;/li&gt;<br/>
   &lt;/ul&gt;<br/>
 </div>
 <hr/>
multi3.addEventListener('click', () => {
    let element = (
        <ul key="ul">
            <li key="A">A</li>
            <li key="B" id="B">B</li>
            <li key="C">C</li>
        </ul>
    );
    ReactDOM.render(element, root);
});
multi3Update.addEventListener('click', () => {
    let element = (
        <ul key="ul">
            <li key="A">A</li>
            <li key="B" id="B2">B2</li>
        </ul>
    );
    ReactDOM.render(element, root);
});

这个就是从ABC都AB的操作,操作如下:

if (newIdx === newChildren.length) {
  deleteRemainingChildren(returnFiber, oldFiber);
  return resultingFirstChild;
}

newIdx和newChildren.length相等,就说明新的已经完成了,那就删除所有的之后的老fiber。完整代码如下:

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    placeChild(newFiber, newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      placeChild(newFiber, newIdx)
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

可以看到删除了C,并且更新了B。接下来看最复杂的一个。

多个节点数量不同、key不同

<div>
 <button id="multi5">9.多个节点数量不同、key不同</button><br/>
   &lt;ul key=&quot;ul&quot;&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;b&quot;&gt;B&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;D&quot;&gt;D&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;E&quot;&gt;E&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;F&quot;&gt;F&lt;/li&gt;<br/>
    &lt;/ul&gt;<br/>
  <button id="multi5Update">处理节点移动的情况</button><br/>
    &lt;ul key=&quot;ul&quot;&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;A&quot;&gt;A&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;C&quot;&gt;C&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;E&quot;&gt;E&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;B&quot; id=&quot;b2&quot;&gt;B2&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;G&quot;&gt;G&lt;/li&gt;<br/>
    &nbsp;&nbsp;&lt;li key=&quot;D&quot;&gt;D&lt;/li&gt;<br/>
    &lt;/ul&gt;<br/>
</div>
<hr/>
multi5.addEventListener('click', () => {
    let element = (
        <ul key="ul">
            <li key="A">A</li>
            <li key="B" id="b">B</li>
            <li key="C">C</li>
            <li key="D">D</li>
            <li key="E">E</li>
            <li key="F">F</li>
        </ul>
    );
    ReactDOM.render(element, root);
});
multi5Update.addEventListener('click', () => {
    let element = (
        <ul key="ul">
            <li key="A">A</li>
            <li key="C">C</li>
            <li key="E">E</li>
            <li key="B" id="b2">B2</li>
            <li key="G">G</li>
            <li key="D">D</li>
        </ul>
    );
    ReactDOM.render(element, root);
});

这种是最复杂的情况,也是DOM DIFF的精华所在,先来梳理下上图的更新逻辑。

先来看下之前的reconcileChildrenArray的逻辑,按照上图的顺序梳理下。

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    placeChild(newFiber, newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      placeChild(newFiber, newIdx)
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }
}

A和A做比较,可以复用;然后比较B和C,key不一样,updateSlot之后返回的null,跳出第一轮循环,newIdx为1,newChildren.length为6,而且oldFiber指向的是B,所以,下面2个判断是进不去的。

if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      placeChild(newFiber, newIdx)
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }

这个时候就按照上面的流程,把剩下的oldFiber放到existingChildren这个map中去,对应的代码如下:

// 将剩下的老fiber放入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
function mapRemainingChildren(returnFiber, currentFirstChild) {
  const existingChildren = new Map();
  let existingChild = currentFirstChild;
  while (existingChild) {
    let key = existingChild.key || existingChild.index;
    existingChildren.set(key, existingChild)
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

这个map目前大概张这样map={B:bFiber,C:cFiber,D:dFiber,E:eFiber,F:fFiber},然后声明一个变量lastPlacedIndex变量,表示不需要移动的老节点的索引,默认为0,接着循环剩下的虚拟dom节点,从C开始。如果能在map中能找到相同key相同type的节点则可以复用老fiber,并把老fiber从map中删除。具体代码如下

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
+  // 指的上一个可以复用的,不需要移动的节点的老索引
+  let lastPlacedIndex = 0;
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    placeChild(newFiber, newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      placeChild(newFiber, newIdx)
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }

+  //将剩下的老fiber放入map中
+  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
+  for (; newIdx < newChildren.length; newIdx++) {
+    //去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
+    const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
+  }
}

生成map

function mapRemainingChildren(returnFiber, currentFirstChild) {
  const existingChildren = new Map();
  let existingChild = currentFirstChild;
  while (existingChild) {
    let key = existingChild.key || existingChild.index;
    existingChildren.set(key, existingChild)
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

从map中去找到可以复用的fiber

function updateFromMap(existingChildren, returnFiber, newIdx, newChild) {
  const matchedFiber = existingChildren.get(newChild.key || newIdx);
  return updateElement(returnFiber, matchedFiber, newChild);
}

接着就看下是否能在map中找到可以复用的,把老fiber从map中删除,找不到的话则创建新的fiber节点。

for (; newIdx < newChildren.length; newIdx++) {
  //去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
  const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
  if (newFiber) {
    //说明是复用的老fiber
    if (newFiber.alternate) {
      existingChildren.delete(newFiber.key || newIdx);
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);这个是比较关键的,newFiber.alternate有值,说明是可以复用的老fiber,需要放置该fiber节点,改造下placeChild方法,之前的代码是这样的:

function placeChild(newFiber, newIdx) {
  newFiber.index = newIdx;
  if (!shouldTrackSideEffects) {
    return;
  }
  const current = newFiber.alternate;
  //如果有current说是更新,复用老节点的更新,不会添加Placement
  if (current) {
    // TODO
  } else {
    newFiber.flags = Placement;
  }
}

改造后的逻辑如下:

function placeChild(newFiber, lastPlacedIndex, newIdx) {
  newFiber.index = newIdx;
  if (!shouldTrackSideEffects) {
    return lastPlacedIndex;
  }
  const current = newFiber.alternate;
  //如果有current说是更新,复用老节点的更新,不会添加Placement
  if (current) {
    const oldIndex = current.index;
    //如果老fiber它对应的真实DOM挂载的索引比lastPlacedIndex小
    if (oldIndex < lastPlacedIndex) {
      //老fiber对应的真实DOM就需要移动了
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      //否则 不需要移动 并且把老fiber它的原来的挂载索引返回成为新的lastPlacedIndex
      return oldIndex;
    }
  } else {
    newFiber.flags = Placement;
    return lastPlacedIndex;
  }
}

之前placeChild用到的地方也做出相应的修改;在复用老的fiber的时候,要老fiber节点的索引与lastPlacedIndex做比较:

  • 如果小于lastPlacedIndex则需要移动老的fiber, lastPlacedIndex不变
  • 如果大于lastPlacedIndex则不需要移动老的fiber,更新lastPlacedIndex为老fiber的index

C在map中能找到,C的索引是2,lastPlacedIndex默认是0,则C是不用移动的,更新lastPlacedIndex为2;同理E也是能在map中找到,也不用移动,E的老索引是4,则更新lastPlacedIndex为4;到B的时候,B的老fiber索引为1,此时lastPlacedIndex为4,那就标记B为移动,lastPlacedIndex不变;到G的时候,在map中找不到可以复用的节点,那就标记为插入;到D的时候,D的老fiber索引为3,lastPlacedIndex为4,标记D为移动,lastPlacedIndex不变,仍然为4。

到这里,新fiber遍历结束了,但是老fiber中的F是没用到的,那就需要把F给删除。那就是在虚拟DOM循环结束后把Map中的所有的剩下的fiber全部标记为删除。

//map中剩下是没有被 复用的,全部删除
existingChildren.forEach(child => deleteChild(returnFiber, child));

完整的代码如下:

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  //将要返回的第一个新fiber
  let resultingFirstChild = null;
  //上一个新fiber
  let previousNewFiber = null;
  //当前的老fiber
  let oldFiber = currentFirstChild;
  //新的虚拟DOM的索引
  let newIdx = 0;
  // 下一个老fiber
  let nextOldFiber = null
  // 指的上一个可以复用的,不需要移动的节点的老索引
  let lastPlacedIndex = 0;
  //处理更新的情况 老fiber和新fiber都存在
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    //先缓存下一个老fiber
    nextOldFiber = oldFiber.sibling;
    //试图复用才fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    //如果key 不一样,直接跳出第一轮循环
    if (!newFiber)
      break; //跳出第一轮循环
    //老fiber存在,但是新的fiber并没有复用老fiber
    if (oldFiber && !newFiber.alternate) {
      deleteChild(returnFiber, oldFiber);
    }
    //核心是给当前的newFiber添加一个副作用flags 叫新增
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (!previousNewFiber) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 如果没有老fiber
  if (!oldFiber) {
    // 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
      // newFiber.flags = Placement
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
    }
    return resultingFirstChild;
  }

  //将剩下的老fiber放入map中
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    //去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
    const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
    if (newFiber) {
      //说明是复用的老fiber
      if (newFiber.alternate) {
        existingChildren.delete(newFiber.key || newIdx);
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (!previousNewFiber) {
        resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
      } else {
        previousNewFiber.sibling = newFiber;//liB.sibling=li(C)
      }
      previousNewFiber = newFiber;//previousNewFiber=>li(C)
    }
  }

  //map中剩下是没有被 复用的,全部删除
  existingChildren.forEach(child => deleteChild(returnFiber, child));

  return resultingFirstChild;
}

reconcileChildrenArray这个方法是DOM DIFF的核心所在,看下运行结果:

到此,DOM DIFF基本结束,其实fiber中有一个调度的过程,根据渲染更新优先级,根据浏览器的响应速度来调度fiber,后面会从这个角度再来看下fiber。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值