React Dom-diff之单节点diff

上篇文章已经了解到了react 的初次渲染,这篇文章来看下react中的单节点Dom-diff

render改造

import { createFiberRoot } from './ReactFiberRoot';
import { updateContainer } from './ReactFiberReconciler';

function render(element, container) {
  //1.创建一个fiberRoot, 它其实指向是我们的div#root这个容器
  let fiberRoot = container._reactRootContainer;
  if (!fiberRoot) {
    fiberRoot = container._reactRootContainer = createFiberRoot(container);
  }
  //把element这个虚拟DOM变成真实DOM插入容器中
  updateContainer(element, fiberRoot);
}

const ReactDOM = {
  render
}
export default ReactDOM;
if (!fiberRoot) {
	fiberRoot = container._reactRootContainer = createFiberRoot(container);
}

这个就确保了渲染的时候赋值,更新的时候取值,确保操作的是同一个fiberRoot

function commitRoot() {
  //指向新构建的fiber树
  const finishedWork = workInProgressRoot.current.alternate;
  workInProgressRoot.finishedWork = finishedWork;
  commitMutationEffects(workInProgressRoot);
}

commitRoot就是提交fiber,修改dom的阶段,dom-diff就在这个阶段。commitMutationEffects中有个分支,就是当fiber的flags是Placement(添加 或者说创建 挂载),会有个commitPlacement的操作,看下之前的代码。

function commitMutationEffects(root) {
  const finishedWork = root.finishedWork;
  let nextEffect = finishedWork.firstEffect;
  let effectsList = '';
  while (nextEffect) {
    effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})`;
    const flags = nextEffect.flags;
    if (flags === Placement) {
      commitPlacement(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }
  effectsList += 'null';
  console.log(effectsList);
  root.current = finishedWork;
}

ReactFiberCommitWork.js

commitPlacement只是其中的一个节点的操作,我们将commitPlacement等相关方法提取到ReactFiberCommitWork.js中去。并且稍作改造。

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

也是用来抹平平台差异性的操作。appendChild方法也来自ReactDOMComponent.js(真实dom操作)

export function appendChild(parentInstance, child) {
    parentInstance.appendChild(child);
}

结果也是一样的,页面上也是渲染出来了title。接下来看下单节点dom-diff是如何实现的。

在beginWork(current, workInProgress)的时候,传入了当前的fiber树和正在构建的fiber树,而且在reconcileChildren的时候,并且是更新操作的时候就会进行比较。

export function reconcileChildren(current, workInProgress, nextChildren) {
  //如果current有值,说明这是一类似于更新的作品
  if (current) {
    //进行比较 新老内容,得到差异进行更新
    workInProgress.child = reconcileChildFibers(
      workInProgress,//新fiber
      current.child,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  } else {
    ///初次渲染,不需要比较 ,全是新的
    workInProgress.child = mountChildFibers(
      workInProgress,//新fiber
      null,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  }
}
/**
   *
   * @param {*} returnFiber 新的父fiber
   * @param {*} currentFirstChild current就是老的意思,老的第一个子fiber
   * @param {*} newChild 新的虚拟DOM
   */
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
    //判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
    const isObject = typeof newChild === 'object' && ( newChild );
    //说明新的虚拟DOM是单节点
    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(reconcileSingleElement(
            returnFiber, currentFirstChild, newChild
          ));
      }
    }
  }

单节点的diff会走到reconcileSingleElement中去,目前reconcileSingleElement还只是直接创建一个fiber节点,并没有对比的过程。

function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  const created = createFiberFromElement(element);//div#title
  created.return = returnFiber;
  return created;
}

先来看下更新情况下,当前current和正在构建的workInProgress的关系图:

current永远是老的fiber树,就是当前页面视图对应的fiber树,workInProgress是当前正在构建的fiber树,他们相互指向,相互引用,就是之前说的双缓冲。

上图是单节点的diff流程图,按这个,我们就分为以下几种情况:

单节点dom-diff

  • key相同,类型相同,复用老节点,只更新属性
  • key相同,类型不同,删除老节点,添加新节点
  • 类型相同,key不同,删除老节点,添加新节点
  • 原来多个节点,现在只有一个节点,删除多余节点

1.原来多个节点,现在只有一个节点,删除多余节点

具体代码实现:

function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  //获取新的虚拟DOM的key
  let key = element.key;
  //获取第一个老的fiber节点
  let child = currentFirstChild;
  while (child) {
    //老fiber的ekey和新的虚拟DOM的key相同
    if (child.key === key) {
      
    } else {
      //如果不相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除
      deleteChild(returnFiber, child);
    }
  }
  const created = createFiberFromElement(element);//div#title
  created.return = returnFiber;
  return created;
}

/**
 * 因为这个老的子fiber在新的虚拟DOM树不存在了,则标记为删除
 * @param {*} returnFiber
 * @param {*} childToDelete
 */
function deleteChild(returnFiber, childToDelete) {
  //如果不需要跟踪副作用,直接返回
  if (!shouldTrackSideEffects) {
    return;
  }
  //把自己这个副作用添加到父effectList中
  //删除类型的副作用一般放在父fiber副作用链表的前面,在进行DOM操作时候先执行删除操作
  const lastEffect = returnFiber.lastEffect;
  if (lastEffect) {
    lastEffect.nextEffect = childToDelete;
    returnFiber.lastEffect = childToDelete;
  } else {
    //父fiber节点effectList是空
    returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
  }
  //清空下一个副作用指向
  childToDelete.nextEffect = null;
  //标记为删除
  childToDelete.flags = Deletion;
}

接下来就是处理下一个fiber,也就是它的sibling。

function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  //获取新的虚拟DOM的key
  let key = element.key;
  //获取第一个老的fiber节点
  let child = currentFirstChild;
  while (child) {
    //老fiber的ekey和新的虚拟DOM的key相同说明
    if (child.key === key) {
    
    } else {
      //如果相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除,并且继续弟弟
      deleteChild(returnFiber, child);
    }
+   // 继续匹配弟弟们
+   child = child.sibling;
  }
  const created = createFiberFromElement(element);//div#title
  created.return = returnFiber;
  return created;
}

2.key相同,类型不同,删除老节点,添加新节点

那此时key相同,按照流程图,就看下type是否相同,如果不相同,则删除包括当前的老fiber在内所所有后续的老fibe

while (child) {
  //老fiber的ekey和新的虚拟DOM的key相同说明
  if (child.key === key) {
    //判断老的fiber的type和新的虚拟DOMtype是否相同
    if (child.type == element.type) {

    } else {
+      //已经配上key了,但是type不同,则删除包括当前的老fiber在内所所有后续的老fibe
+      deleteRemainingChildren(returnFiber, child);
+      break;
    }
  } else {
    //如果不相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除
    deleteChild(returnFiber, child);
    child = child.sibling;
  }
}

复用child老fiber节点,删除剩下的其它fiber

function deleteRemainingChildren(returnFiber, childToDelete) {
  while (childToDelete) {
    deleteChild(returnFiber, childToDelete);
    childToDelete = childToDelete.sibling;
  }
}

3.key相同,类型相同,复用老节点,只更新属性

type如果是相同的,那就可以直接使用原来的fiber,并且更新属性

while (child) {
  //老fiber的ekey和新的虚拟DOM的key相同说明
  if (child.key === key) {
    //判断老的fiber的type和新的虚拟DOMtype是否相同
    if (child.type == element.type) {
+      //准备复用child老fiber节点,删除剩下的其它fiber
+      deleteRemainingChildren(returnFiber, child.sibling);
+      //在复用老fiber的时候,会传递新的虚拟DOM的属性对象到新fiber的pendingProps上
+      const existing = useFiber(child, element.props);
+      existing.return = returnFiber;
+      return existing;
    } else {
      //已经配上key了,但是type不同,则删除包括当前的老fiber在内所所有后续的老fibe
      deleteRemainingChildren(returnFiber, child);
      break;
    }
  } else {
    //如果相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除,并且继续弟弟
    deleteChild(returnFiber, child);
  }
  //继续匹配弟弟们
  child = child.sibling;
}

复用老fiber,并清空弟弟

function useFiber(oldFiber, pendingProps) {
  let clone = createWorkInProgress(oldFiber, pendingProps);
  clone.sibling = null;//清空弟弟
  return clone;
}

这些操作是执行在beginWork中,并没有正真的去删除掉,只是在这个地方做了标记,在commit阶段才会正真的去处理。

completeWork重构

export function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostComponent:
      // 在新的fiber构建完成的时候,收集更新并且标识 更新副作用
      if (current && workInProgress.stateNode) {
        updateHostComponent(current, workInProgress, workInProgress.tag, newProps);
      } else {
        //创建真实的DOM节点
        const type = workInProgress.type;//div p span
        //创建此fiber的真实DOM
        const instance = createInstance(type, newProps);
        //让此Fiber的真实DOM属性指向instance
        workInProgress.stateNode = instance;
        //给真实DOM添加属性 包括如果独生子是字符串或数字的情况
        finalizeInitialChildren(instance, type, newProps);
      }
      break;
    default:
      break;
  }
}

current && workInProgress.stateNode 就说明是更新操作了,在更新阶段,执行updateHostComponent

/**
 *
 * @param {*} current 老fiber
 * @param {*} workInProgress 新fiber
 * @param {*} tag
 * @param {*} newProps 新的虚拟DOM上的新属性
 */
function updateHostComponent(current, workInProgress, tag, newProps) {
  //老fiber上的老属性
  let oldProps = current.memoizedProps;
  //可复用真实的DOM节点
  const instance = workInProgress.stateNode;
  const updatePayload = prepareUpdate(instance, tag, oldProps, newProps);
  workInProgress.updateQueue = updatePayload;
  if (updatePayload) {
    workInProgress.flags |= Update;
    //当flags=6的话,就是既要插入新位置 ,又要更新,针对移动 节点的情况
  }
}

prepareUpdate方法,主要是用来更新属性。

export function prepareUpdate(domElement, type, oldProps, newProps) {
  return diffProperties(
    domElement,
    type,
    oldProps,
    newProps
  );
}
export function diffProperties(domElement, tag, lastProps, nextProps) {
  let updatePayload = null;
  let propKey;
  for (propKey in lastProps) {
    if (lastProps.hasOwnProperty(propKey) && ( !nextProps.hasOwnProperty(propKey) )) {
      //updatePayload更新数组 [更新的key1,更新的值1,更新的key2,更新的值2]
      ( updatePayload = updatePayload || [] ).push(propKey, null);
    }
  }
  for (propKey in nextProps) {
    const nextProp = nextProps[propKey];
    if (propKey == 'children') {
      if (typeof nextProp === 'string' || typeof nextProp === 'number') {
        if (nextProp !== lastProps[propKey]) {
          ( updatePayload = updatePayload || [] ).push(propKey, nextProp);
        }
      }
    } else {
      //如果新的属性和老的属性不一样
      if (nextProp !== lastProps[propKey]) {
        ( updatePayload = updatePayload || [] ).push(propKey, nextProp);
      }
    }
  }
  return updatePayload;
}

接下来,还需要处理commitMutationEffects,这个是commitRoot中执行的,之前只处理了Placement的情况,其他情况也需要处理。

function commitMutationEffects(root) {
  const finishedWork = root.finishedWork;
  let nextEffect = finishedWork.firstEffect;
  let effectsList = '';
  while (nextEffect) {
    effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})=>`;
    const flags = nextEffect.flags;
    let current = nextEffect.alternate;
    if (flags === Placement) {
      commitPlacement(nextEffect);
    } else if (flags === PlacementAndUpdate) {
      commitPlacement(nextEffect);
      nextEffect.flags = Update;
      commitWork(current, nextEffect);
    } else if (flags === Update) {
      commitWork(current, nextEffect);
    } else if (flags === Deletion) {
      commitDeletion(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }
  effectsList += 'null';
  console.log(effectsList);
  root.current = finishedWork;
}

commitDeletion 删除操作

export function commitDeletion(fiber) {
  if (!fiber) return;
  let parentStateNode = getParentStateNode(fiber);
  removeChild(parentStateNode, fiber.stateNode);
}
export function removeChild(parentInstance, child) {
    parentInstance.removeChild(child);
}

commitWork提交DOM更新操作

/**
 * 提交DOM更新操作
 * @param {*} current
 * @param {*} finishedWork
 */
export function commitWork(current, finishedWork) {
  const updatePayload = finishedWork.updateQueue;
  finishedWork.updateQueue = null;
  if (updatePayload) {
    updateProperties(finishedWork.stateNode, updatePayload);
  }
}

updateProperties更新属性

export function updateProperties(domElement, updatePayload) {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === 'children') {
      domElement.textContent = propValue;
    } else {
      domElement.setAttribute(propKey, propValue);// id
    }
  }
}

这个地方i+=2是因为updatePayload更新数组是 [更新的key1,更新的值1,更新的key2,更新的值2]这样的存储数据格式;

测试1:

index.html

<div>
  <button id="single1">1.key相同,类型相同,数量相同</button><br/>
  &lt;div key=&quot;title&quot; id=&quot;title&quot;&gt;<br/>
    &nbsp;&nbsp;div<br/>
  &lt;/div&gt;<br/>
  <button id="single1Update">复用老节点,只更新属性</button><br/>
  &lt;div key=&quot;title&quot; id=&quot;title2&quot;&gt;<br/>
    &nbsp;&nbsp;div2<br/>
  &lt;/div&gt;
</div>
<hr/>

index.js

//1. key相同,类型相同,复用老节点,只更新属性
single1.addEventListener('click', () => {
    let element = (
        <div key="title" id="title">title</div>
    );
    ReactDOM.render(element, root);
});
single1Update.addEventListener('click', () => {
    let element = (
        <div key="title" id="title2">title2</div>
    );
    ReactDOM.render(element, root);
});

页面结果:

控制台也可以清楚的看到插入和更新的操作。

测试2:

<div>
  <button id="single2">2.key相同,类型不同</button><br/>
  &lt;div key=&quot;title&quot; id=&quot;title&quot;&gt;<br/>
  &nbsp;&nbsp;div<br/>
  &lt;/div&gt;<br/>
  <button id="single2Update">删除老节点,添加新节点</button><br/>
  &lt;p key=&quot;title&quot; id=&quot;title&quot;&gt;<br/>
  &nbsp;&nbsp;p<br/>
  &lt;/p&gt;<br/>
</div>
<hr/>
//2.key相同,类型不同,删除老节点,添加新节点
single2.addEventListener('click', () => {
  let element = (
    <div key="title" id="title">title</div>
  );
  ReactDOM.render(element, root);
});
single2Update.addEventListener('click', () => {
  let element = (
    <p key="title" id="title">title</p>
  );
  ReactDOM.render(element, root);
});

可以看到先执行了删除div的操作,并且执行了插入P标签的操作。

测试3:

<div>
  <button id="single3">3.类型相同,key不同</button><br/>
  &lt;div key=&quot;title1&quot; id=&quot;title&quot;&gt;<br/>
  &nbsp;&nbsp;title<br/>
  &lt;/div&gt;<br/>
  <button id="single3Update">删除老节点,添加新节点</button><br/>
  &lt;div key=&quot;title2&quot; id=&quot;title&quot;&gt;<br/>
  &nbsp;&nbsp;title<br/>
  &lt;/div&gt;<br/>
</div>
<hr/>
single3.addEventListener('click', () => {
  let element = (
    <div key="title1" id="title">title</div>
  );
  ReactDOM.render(element, root);
});
single3Update.addEventListener('click', () => {
  let element = (
    <div key="title2" id="title">title</div>
  );
  ReactDOM.render(element, root);
});

类型相同,key不同,删除老节点,添加新节点,先执行了删除div的操作,并且执行了插入div标签的操作。

测试4:

<div>
  <button id="single4">4.原来多个节点,现在只有一个节点</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="single4Update">保留并更新这一个节点,删除其它节点</button><br/>
  &lt;ul key=&quot;ul&quot;&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/>
//4.原来多个节点,现在只有一个节点,删除多余节点
single4.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);
});
single4Update.addEventListener('click', () => {
    let element = (
        <ul key="ul">
            <li key="B" id="B2">B2</li>
        </ul>
    );
    ReactDOM.render(element, root);
});

测试发现,第一次渲染都没出来结果:

4.原来多个节点,现在只有一个节点,删除多余节点

这个是因为reconcileChildFibers的时候,目前只处理了单节点的情况,看代码:

function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
  //判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
  const isObject = typeof newChild === 'object' && ( newChild );
  //说明新的虚拟DOM是单节点
  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(reconcileSingleElement(
          returnFiber, currentFirstChild, newChild
        ))
    }
  }
}

如果newChild是数组的话,是没有处理的,接下来处理这个

function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
  //判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
  const isObject = typeof newChild === 'object' && ( newChild );
  //说明新的虚拟DOM是单节点
  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(reconcileSingleElement(
          returnFiber, currentFirstChild, newChild
        ))
    }
  }

  // 处理多个虚拟DOM
  if (Array.isArray(newChild)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
  }
}

reconcileChildrenArray

/**
 * 如果新的虚拟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)
      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;
  }
}
function createChild(returnFiber, newChild) {
  const created = createFiberFromElement(newChild);
  created.return = returnFiber;
  return created;
}

createChild是根据新的虚拟dom创建一个fiber(created),并将它的return(父亲)指向传进来的returnFiber,并返回。

按照上图的fiber新旧节点来看,第一轮循环就是创建A,previousNewFiber肯定不存在,resultingFirstChild(将要返回的第一个新fiber)就指向了A,previousNewFiber也指向了A;第二轮的时候previousNewFiber有值,指向的是A,那就是liA.sibling=li(B),previousNewFiber指向了li(B);第三轮的时候,以此内推,liB.sibling=li©,previousNewFiber指向li©。

执行之后,可以看到有ul插入,但是没有li,这个是为啥呢?原因是在第一次reconcileChildren的时候,也就是在mountChildFibers的时候,current是null,current.child自然也不存在,应该是null,是null的话,export const mountChildFibers = childReconciler(false);shouldTrackSideEffects这个值就是false,没有给新的fiber添加flags,li自然是没有操作的。

function placeSingleChild(newFiber) {
  //如果当前需要跟踪副作用,并且当前这个新的fiber它的替身不存在
  if (shouldTrackSideEffects && !newFiber.alternate) {
    //给这个新fiber添加一个副作用,表示在未来提前阶段的DOM操作中会向真实DOM树中添加此节点
    newFiber.flags = Placement;
  }
  return newFiber;
}

下面是修改前后的对比:

export function reconcileChildren(current, workInProgress, nextChildren) {
  //如果current有值,说明这是一类似于更新的作品
  if (current) {
    //进行比较 新老内容,得到差异进行更新
    workInProgress.child = reconcileChildFibers(
      workInProgress,//新fiber
      current.child,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  } else {
    ///初次渲染,不需要比较 ,全是新的
    workInProgress.child = mountChildFibers(
      workInProgress,//新fiber
-      current && current.child,//老fiber的第一个子fiber节点 
+      null,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  }
}

我们可以在reconcileChildrenArray循环newChildren的时候,手动加上newFiber.flags = Placement

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;
  }
}

然后再来看下结果:

可以看到插入的顺序和结果,但是源码中初次渲染的并没有搜集副作用,而是在完成的时候(completeWork)的时候调用appendAllChildren(instance, workInProgress);

export function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostComponent:
      //在新的fiber构建完成的时候,收集更新并且标识 更新副作用
      if (current && workInProgress.stateNode) {
        updateHostComponent(current, workInProgress, workInProgress.tag, newProps);
      } else {
        //创建真实的DOM节点
        const type = workInProgress.type;//div p span
        //创建此fiber的真实DOM
        const instance = createInstance(type, newProps);
+        appendAllChildren(instance, workInProgress);
        //让此Fiber的真实DOM属性指向instance
        workInProgress.stateNode = instance;
        //给真实DOM添加属性 包括如果独生子是字符串或数字的情况
        finalizeInitialChildren(instance, type, newProps);
      }
      break;
    default:
      break;
  }
}
function appendAllChildren(parent, workInProgress) {
  let node = workInProgress.child;
  while (node) {
    if (node.tag === HostComponent) {
      //把大儿子的真实DOM节点添加到父真实DOM上
      appendChild(parent, node.stateNode);
    }

    if (node === workInProgress) {
      return;
    }
    while (!( node.sibling )) {
      if (!( node.return ) || node.return === workInProgress) {
        return;
      }
      node = node.return;
    }
    node = node.sibling;
  }
}

正真的DOM操作appendChild

export function appendChild(parentInstance, child) {
  parentInstance.appendChild(child);
}

然后去掉newFiber.flags = Placement这个,再来看下结果:

最后看下更新的操作结果:

到这里,单节点的dom-diff已经是实现,测试也是ok的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值