React 是如何协调的 ?

211 阅读16分钟

著有《React18 设计原理》《javascript地月星》等多个专栏。 欢迎关注。

创作不易,内容有帮助记得 ❤️点赞,⭐️收藏 ,🔥评论 ~

本文全部都是原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解 ~

推荐指数(满级):⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️

github 👉github.com/chd666233/b…
主页 👉juejin.cn/post/757791…

前言

"协调"包含了创建 wip 节点和给节点打上标记。
本文的核心就两个,创建wip节点。另一个是协调的算法,给wip打上标记。
其中,创建wip,首先判断节点是否有变化,然后判断是否能复用cur和cur.alternate节点,最后创建出wip。
判断节点是否有变化,使用props+cur.lanes。判断是否能复用使用key/index + elementType、type。

总则

beginWork 负责构建 wip 节点的下一级节点。

  1. 视角看向 wip 树和 cur 树。首先对比 cur.props 和 wip.props 是否相同,其次确定本次协调的等级,是否包含了cur.lanes。对于不在本轮协调等级的节点,都跳过,等下一次协调。
    props 没有变化 + lanes 没有任务=快速退出
    props 没有变化 + lanes 有任务=协调 (父组件没有变化,子组件自己发生变化的情况)
    props 有变化 = 协调
  2. 视角看向 jsx 和 cur 树 cur 节点。对比cur.key/cur.index 与 ele.key/index;再对比cur.elementType 与 ele.type。检查是否可以复用 cur 节点。再看看这个 cur.alternate 存在不存在,复用它作为 wip Fiber。否则,new 一个新的 wip Fiber 作为 cur.altnate。(这里的 element 是 从 jsx 生成的react element)

遍历当前wip,创建子节点.png

双树切换:提交后 alternate 仍被保留,cur 和 wip 来回切换复用

A(current) <-- alternate --> A'(wip),提交渲染、切换Fiber树后,除了wip树变成了新的current树,这个时候,alternate仍被保留,旧current节点仍被指向。

上一轮双节点:A(旧current) <-- alternate --> A'(当前current)
这一轮双节点:A(当前current) <-- alternate --> A'(旧current、新wip),———旧current树相对应的变成了新wip树。———双缓冲树在相同的cur节点和wip节点来回切换复用。

总之,beginWork 是以复用旧节点的前提,创建 wip Fiber。这样会在同一棵双树的基础上来回切换复用。知晓这个目的,就能比较容易的读懂代码。

遍历当前节点,创建子节点,提供下一级遍历节点

遍历当前wip的时候构建子级wip Fiber ——— 提前一级创建。
你站在当前wip位置或许会问:当前的wip 节点创建子节点,那当前的wip 节点在什么时候创建的?
答案是在遍历到当前 wip 的父节点的时候,当前wip 节点被创建了。

启动项目
创建根容器、根wip, 从根wip开始循环wip树,
👉beginWork wip
wip.child = createWip //构建
createWip:wip.child.child = cur.child.child
return wip.child
👉beginWork wip.child //使用
wip.child.child = createWip //构建
createWip:wip.child.child.child = cur.child.child.child
return wip.child.child
👉beginWork wip.child.child //使用
...

这是为什么beginWork负责创建子wip。但是,实际上 wip 的子节点不一定是空的,所以还存在复用旧的 wip 节点的情况。

下文,我们都是遍历到当前节点,进入 beginWork 的时候,它的子节点,wip.child + sibling(wip子) 还没有实际创建,处在 react ele 形态。


第一阶段 workInProgress 树和双树的创建

遍历一个Fiber节点,开始遍历阶段(beginWork)包含:

  • 情况一:bailout 快速退出 - 复用 cur 节点 - 复用/新建 cur.alternate
  • 情况二:reconcile 协调 - 对比 cur 与 react ele “查询”可以复用的 cur 节点 - 复用/新建 cur.alternate

它们都是在做下一级节点的创建,——— 即 child 节点、sibling 节点的创建。并让它们知道父节点是谁,即它们的 return。
同时构建双缓冲树,即它们的 alternate 指向。最后在提交阶段、Fiber树的切换时,只要简单FiberRootNode.current = workInProgressRoot就可以切换,就是因为alternate在遍历中构建好了。

判断组件/元素是否发生变化

“快速退出”是指 jsx 和 cur 子树结构一样,可以复用 cur 的结构,子树跳过协调:

  • 父组件没有变化(于是这个组件 newProps === oldProps)并且组件没有任务(lanes)。
  • 如果是元素类型,元素 ele 没有变化(newProps === oldProps)。
  • 跳过协调不等于什么都不用做,依然要构建 wip 树。只不过,“跳过协调”只要按顺序复用 cur.child + sibling ——— cur子节点、cur子.alterante节点(例如 cur.child.alternate)就可以。

伪代码:bailout = cloneChildFibers = createWorkInProgress
cloneChildFibers 负责完成 child 和 sibling 变量的赋值,child/sibling 的值来自从 createWorkInProgress 返回的 wip Fiber。
createWorkInProgress 负责返回一个 wip Fiber,如果 cur.alternate 存在,复用 cur.alternate 作为 wip Fiber。如果不存在要就要 new 一个 wip Fiber 返回了。

“协调”是指组件发生了变化,子树不能跳过协调:

  • 父组件发生了变化(newProps !== oldProps)或者组件自身状态发生变化(lanes),出现一个任务,例如等于lanes = SyncLane(值为1,同步级任务)。
  • 如果是元素类型,元素 ele 发生了变化(newProps !== oldProps)。
  • 需要“查询”与 react ele 相同的 cur 子节点复用

伪代码:reconcile = reconcileChildFibers + reconcileChildrenArray。
reconcileChildFibers 给 child 和 sibling 变量赋值。
reconcileChildrenArray 负责创建wipFiber:
1.useFiber + createWorkInProgress
2.createFiber + new FiberNode(tag, pendingProps, key, mode)
情况 1 如果有cur节点,复用 cur,使用useFiber,useFiber使用createWorkInProgress 创建/复用 wip Fiber。
情况 2 如果没有cur节点,使用new FiberNode创建新的wip Fiber。

总之,它们都是先看有没有cur,再看有没有cur.alternate。复用可以复用的wip Fiber,否则新建新的wip Fiber。 区别是,“快速退出”的cur子树和需要构建的wip子树是一样的,所以不需要看有没有cur,只看有没有cur.alternate。

“查询”与 react ele 相同的 cur 子节点

bailout:cur 与 react ele 结构相同,按顺序复用。

reconcile:react ele 可能经历了删除、移动、新增,与cur结构不一样。cur.index/key和ele.index/key对比,cur.elementType和 ele.type对比。如果没有设置key,就需要使用index了。——— 只要index/key + elementTypes、type相同就认为是相同的。

场景:没有设置 key 的影响。

当前屏幕上的 DOM: [C'],wip:[C],cur:[C'],wip C <-- alternate --> cur C'。没有设置 key。现在,新的react element : [A B C'],A 和 B 是新增的。如果ele A 和 cur C'的 index 和 elementType ('div') 相同,会变成 wip: [C 更新为 A 的值,新建 B ,新建 C'],———也就是说,复用C作为A的wip Fiber,变成wip A(旧C更新) <-- alternate --> cur C'。但是理想的方式应该是“保持关系”:复用 C 作为 C'的 wip Fiber,wip C<--alternate-->cur C'。C C'依然是一对。这就需要设置 key。


再论 wip Fiber 树的构建 ——— 判断子树是否变化

从 beginWork 源码中可以看到这样的比较:

  1. oldProps !== newProps,reconcile。
  2. cur.lanes 包含在本轮 renderLanes,bailout。表示该节点本轮协调没有任务。
  3. cru.lanes 不包含在本轮 renderLanes,reconcile。表示有任务。

还有2个本文省略的条件
hasContextChanged()上下文
workInProgress.type !== current.type 节点类型,组件节点是组件函数function xx(){} 元素节点是div、p

newProps 和 oldProps 就是 workInProgress.pendingProps 和 current.memoizedProps。

简单的说,上面的比较就是:
判断一个组件是否发生变化:先比较 props,再检查 lanes。
判断一个元素是否发生变化:只需要比较 props。因为元素没有自己的状态。

介绍

  1. workInProgress.pendingProps 是接下来需要渲染到页面上的 Fiber 节点属性
  2. current.memoizedProps 是当前显示在页面上的 Fiber 节点属性。
  3. renderWithHooks(,,Component,,,) 生成react element对象。只在组件使用,以组件为单位生成 react ele。
var element = {//react ele
  $$typeof: REACT_ELEMENT_TYPE,
  type: type, //'div'
  key: key, //null
  ref: ref, //null
  props: props, //props是节点的属性。属性/事件/样式/子节点
  _owner: owner
};
  1. Component 组件的函数声明:
function MyComp(){
    let [xx,setXX] = useState()
    return <div onClick=setXX><div><p>{xx}</<p></div></div>
}

<MyComp />
  1. lanes 表示节点有任务,就像微信的“未读消息+1”标签,不知道具体的消息内容,只知道有消息。要“点进去看”才知道具体的消息内容,即遍历Fiber树的时候,看到有lanes,就知道需要协调子树了。

例子

react ele 节点与它的属性 props

function Comp(){
    return (<div 属性/事件/样式/...>子节点...</div>)
}

var element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type, //'div'
    key: key, //null
    ref: ref, //null
    props: props, //props是节点的属性。属性/事件/样式/子节点
    _owner: owner
  };

props = {children, style, onXX, 其他属性}
-   props.children // 子级element
-   props.fallback; // 如果是Suspense,有fallback
-   props.style //样式;
-   props.onXXX //事件
-   props.xxx //其他属性

组件也是这样子的:
<MyComp 属性/事件/样式...></MyComp>
不过我们不会在组件上直接写子节点:
<MyComp 属性/事件/样式...>❌子节点...</MyComp> 不会显示这个子节点。

React 的响应粒度是组件级别的,如 MyComp 👆

这个概念表达的是,节点是否有变化、选择 bailout 还是 reconcile 的判断方式——— lanes 和 ele.props,这个判断方式是组件级别的。

组件 useState 发生了变化,

首先,遍历 MyComp Fiber,首先比较新旧节点属性:这个组件的父组件没有变化,所以这个组件的 oldProps === newProps。然后检查这个组件的 lanes: 这个组件 state 有变化,所以组件的 lanes 是被标记过的。于是,这个组件生成新的 react ele、组件内部元素的 pendingProps 变成新的对象。

现在,遍历组件内的元素 Fiber,同样的流程:使用 ele.prors 判断,是组件重新生成的,于是 oldProps !== newProps,整个组件都需要 reconcile。——— 这个判断没有做到精准感知元素 Fiber 自己的 lanes而是使用整个组件的 react ele。只有 p 使用了 state,其实只要协调 p 就好了,但实际上包括 div 全部都 reconcile 了。不会 bailout div,然后 reconcile p,而是整个组件所有的元素都reconcile。

这个过程中, react ele 是以组件为单位生成的,只有组件类型的 Fiber 会执行 renderWithHooks(整个应用的 jsx -> react ele 不是一次性生成的。而是按组件生成,遍历到组件 Fiber 的时候生成组件的 ele)。至于 lanes,只有组件 lanes 会有效。

产生的问题:1. 如果一个很庞大的组件(元素数量很多,层级很深),它只有 1 个状态发生了简单的变化,并且只有 1 个子元素中简单的使用了,但却会导致整个组件,包括子组件全部重新渲染,即使子组件没有变化。2. 如果子组件也有变化,但是等级不高,按设计意图,不同等级 lanes 应该分批处理,但父组件的更新把子组件一起更新了,违背了分级处理的设计意图。例如父组件 lanes =1,子组件 lanes=64。

这两种情况都会有性能问题。对于 1,应该把组件变小,或者把这个状态分离到独立子组件。对于 2,应该给子组件缓存。

状态的声明和状态的使用最大的问题:只有组件有 state。元素 Fiber只能没有自己的 state。元素Fiber只能使用组件 Fiber 的 state。

  1. 组件 Fiber,能在自己函数“体内”写useState,lanes 能标记。
  2. 元素 Fiber,没有自己的函数声明去写 useState;元素使用的是组件的 state;这不算作自己的 lanes。例如 p没有自己的“身体”写useState。p 使用的是组件的状态。

所以,虽然 lanes 是标记节点有更新,但实际上元素的 lanes 一直是 0。 (React 有时候会直接把组件的 lanes 同步给组件内元素的 lanes,但实际上元素 lanes 没什么用处)

useState是组件级别的,这就是为什么 lanes 一定是组件级别的。

props、react ele 和 lanes 都来自 state

  1. 点击 --> 组件 state --> lanes!=0 (lanes 来自 state)
  2. 点击 --> 组件 state --> lanes!=0 --> 组件 react ele 重新生成 (ele 来自 state)
  3. 点击 --> 组件 state --> lanes!=0 --> 组件 react ele 重新生成 --> 子组件 props 是新的对象 --> 子组件 react ele 也重新生成 (props 来自 state)
    (<MyComp2 view = {xx}/>,如果子组件使用了 props,props 就是父组件 state。此处说的props是这个props。不是react ele.props)
  4. 另一种变化来源是上下文 context-provider

可以认为,它们都是组件级别的。

beginWork代码

function beginWork(current, workInProgress, renderLanes) {
  ...
  //⚠️ oldProps !== newProps、lanes==0、lanes!==0(hasScheduledUpdateOrContext)
  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    //props发生变化协调
    if(oldProps !== newProps || hasContextChanged() || (workInProgress.type !== current.type )){
      didReceiveUpdate = true;
    } else {
      //props没有发生变化的
      //但是本轮更新的lanes包含了cur.lanes,也算有变化,这个就是例如父组件没有变化,子组件有变化。
      //在子组件也没有变化下,新旧fiber结构肯定一样,使用快速退出
      //快速退出
      var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
      if (!hasScheduledUpdateOrContext && 
          (workInProgress.flags & DidCapture) === NoFlags) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
      } 
      //cur.lanes有变化子组件发生了变化
      ...
    }
  } else {
    ...
  }

  // 进行协调
  switch (workInProgress.tag) {
      ...
    case LazyComponent:
      {
        var elementType = workInProgress.elementType;
        return mountLazyComponent(current, workInProgress, elementType, renderLanes);
      }

    case FunctionComponent:
      {
        var Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
      }
      ...
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
      ...
  }
}
//⚠️情况一:
function attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes) {
  ...
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  ...
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { 
    {
      return null;
    }
  } 

  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

function cloneChildFibers(current, workInProgress) {
 // 用workInProgress.child !== current.child判断还没有构建好wip.child
 if (current !== null && workInProgress.child !== current.child) {
    throw new Error('Resuming work not yet implemented.');
  }

  if (workInProgress.child === null) {
    return;
  }
  var currentChild = workInProgress.child;
  var newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  //⚠️构建child、return
  workInProgress.child = newChild // child
  newChild.return = workInProgress;// return

  //⚠️构建sibling
  while (currentChild.sibling !== null) {//兄弟节点createWorkInProgress
    currentChild = currentChild.sibling; //sibling
    newChild = newChild.sibling = createWorkInProgress(currentChild, currentChild.pendingProps);
    newChild.return = workInProgress;// return  
  }
  newChild.sibling = null;
}
//有cur。有没有wip?——— 复用Fiber/新建Fiber
function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;

  if (workInProgress === null) {
    //新的Fiber的出现
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);//
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    ...
    //⚠️构建双缓冲树
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    //复用Fiber
    workInProgress.pendingProps = pendingProps;// 
    workInProgress.type = current.type; 
    workInProgress.flags = NoFlags; 

    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
    ...
  } 

  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;
  workInProgress.child = current.child;//⚠️
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue; 
  ...  
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;
  ...

  return workInProgress;
} 
//⚠️情况二:
function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) {
  ...
  nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes);
  ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
  ...
  var children = Component(props, secondArg); //function App(){...  return React.createElement('div',{},React.createElement('p',{}, 'xx'))}
  ...
  return children;
}

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  if (current === null) {
    //⚠️构建wip.child 新的Fiber的出现
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    //⚠️构建wip.child
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
  ...
  if (typeof newChild === 'object' && newChild !== null) {
    ...
    if (isArray(newChild)) {
      //⚠️:构建wip.child.sibling、wip.child.return
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }
      ...
  }
    ...
}

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
  {
    //遍历一遍,节点没有设置key的警告 validate keys.
    var knownKeys = null;
    for (var i = 0; i < newChildren.length; i++) {
      var child = newChildren[i];
      knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
    }
  }
  ...
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    ...
    //⚠️使用key+elementType查找cur,构建wip Fiber
    var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
      ...
      break;
    }
      ...
      //diff,给wip Fiber打上Placements
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    ...
  }
  ...⚠️包含构建sibling省略

  //⚠️使用key/index + elementType查找cur,构建wip Fiber
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber); 

  for (; newIdx < newChildren.length; newIdx++) {
    var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);

    if (_newFiber2 !== null) {
      if (shouldTrackSideEffects) {
        if (_newFiber2.alternate !== null) {
          existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
        }
      }
        //给wip Fiber打上Placements
      lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
      ...
    }
  }
  ...
  return resultingFirstChild;
}

//cur.key  jsx:newChild.key
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
  var key = oldFiber !== null ? oldFiber.key : null;
  ...
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        {
          //⚠️判断key 
          if (newChild.key === key) {
            return updateElement(returnFiber, oldFiber, newChild, lanes);
          } else {
            return null;
          }
        }
        ...
    }
    ...       
  }
}
// 复用Fiber/新的Fiber  cur.elementType  jsx:element.type
function updateElement(returnFiber, current, element, lanes) {
  var elementType = element.type;
  ...
  //⚠️判断有没有current
  if (current !== null) {
    // ⚠️判断elementType
    if (current.elementType === elementType || (
      isCompatibleFamilyForHotReloading(current, element) ) || 
        typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === current.type) {
      // Move based on index
      var existing = useFiber(current, element.props);
      existing.ref = coerceRef(returnFiber, current, element);
      //⚠️ 构建wip.child.return
      existing.return = returnFiber;
      ...
      return existing;
    }
  }
  //有current,复用cur,检查是否能复用cur.alternate的方式返回wip
  function useFiber(fiber, pendingProps) {
    var clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
  }
  
  //没有current,直接创建新的wip Fiber
  var created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.ref = coerceRef(returnFiber, current, element);
   //⚠️ 构建wip.child.return
  created.return = returnFiber;
  return created;
}
function createFiberFromElement(element, mode, lanes) {
  ...
  var type = element.type;
  var key = element.key;
  var pendingProps = element.props;
  var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);
  ...
  return fiber;
}

function createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes) {
  ...
  var fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  ...
  return fiber;
}

var createFiber = function (tag, pendingProps, key, mode) {
  return new FiberNode(tag, pendingProps, key, mode);
};
function FiberNode(tag, pendingProps, key, mode) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null; // Fiber

  this.return = null;
  this.child = null; //⚠️
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.pendingProps = pendingProps;//需要显示的react element
  this.memoizedProps = null;//当前显示的react element
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode; // Effects

  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null;
  ...
} 

第二阶段 协调和提交

1.复原、新建、删除

源码包含了 1. updateSlot "查询"然后复用、查不到然后新建 2. placeChild/..标记 flags。nextChildren 变量就是 react ele。 A-B 协调.png

  • 复原:cur.lanes=0,没有任务,wip需要创建一个和cur一样的节点。但是wip flags不标记Placement,因为屏幕上有这个DOM节点。
  • 新增:cur.lanes=1,有任务,进入协调,看看是什么变化,react ele 有 cur 没有,创建新的。 updateSlot 函数返回 wip Fiber。新增也要通过 placeChild 函数给 wip 打上 Placement flags。
  • 删除:虽然 wip 树上本来没有这个 Fiber,但是屏幕上仍然有,因此不是 wip 树不创建这个 Fiber 就可以的,还需要从屏幕上删除,通过在父节点帮忙记录,提交的时候才知道删除。

2. 移动 通过 placeChild 给 wip 打上 Placement(新增/移动)标记

A-B 协调2.png

出乎意料的,我认为是 “3 往前移动,1,2 没有移动”,但是 React 的思路是 “3 没有动,1,2 往后移动”。

注意,红色的箭头其实不应该出现在这里的,红色箭头是提交阶段的内容。我们这里还在协调阶段 ——— placeChild 给 wip 打上 Placement 标记。

协调阶段:1. 从 react ele 构建 wip Fiber(绿色和黄色节点),2. 打上 Placement(P)。
提交阶段:3.利用 P 标记的节点,计算移动位置。从旧的位置,新的位置,计算 DOM 的移动方式(红色箭头)。

注意,红色箭头描述的是 Fiber 节点对应的 DOM 的移动方式。 例如:✅1 的 DOM 插入到 7 的 DOM 前面。❌而不是 1 插入到 7 前面。我想表达的是:图上的节点都是 Fiber 节点,且位置是正确,按照 react ele 的顺序生成的。不要傻乎乎的去移动 Fiber,要移动的是显示在屏幕上的 DOM。

Q:react ele 是最新正确的,直接用 ele 生成 wip Fiber 就好了,为什么要 reconcile 还要 placeChild?

“查询” cur 与 ele “相同”的 cur,复用能复用的 cur.alternate,最终复用可以复用的 DOM(cur.alternate.stateNode),最终才能减少 DOM 操作。要达到复用 DOM 不得不复用 Fiber。
复用了 wipFiber,但是不知道 wipFiber 怎么移动到这个位置的。placeChild 这里对比 newFiber 和 oldFiber 的 index,打上标记,DOM 才知道 wipFiber 是怎么移动到这个位置的。
placeChild 仅仅给 wip Fiber 打上标记,还没有操作 DOM,真正的操作 DOM 在提交阶段,利用这里计算的 Placements 标记计算 DOM 位置。

Q: 移动怎么知道移动多少位?“这个节点向前移动 2 位,3 位...”似乎没有进行移动位数的计算?

提交阶段不用计算移动多少位。而是知道 insertBefore(A, B) A 插入到 B 的前面。 getHostSibling(A) 找 A 后面最近的,没有 Placement 标记的节点,就是 B。 A 是当前提交流程中遍历的。例如 2 3 都是 A。7 是 B。

Q: 为什么比直接操作 DOM 性能高?

cur 树和 wip 树操作的是同一份 DOM。避免反复新建 DOM。

3. 更新 与 Update Flags

其实删除、移动、新增都属于更新,另一种是“文本颜色等内容更新”,这属于简单的 state 变化,newProps !== oldProps 能判断出来不是同一个 react ele。协调里面不需要进一步对比内容是否发生变化,专门标记 Update,因为复用或者新建的 wipFiber 都会同步 react ele 信息,生成最新的 wip Fiber。

协调:beginWork 阶段代码(移动和新增,不包含删除)

function placeChild(newFiber, lastPlacedIndex, newIndex) {
  newFiber.index = newIndex;

  if (!shouldTrackSideEffects) {
    newFiber.flags |= Forked;
    return lastPlacedIndex;
  }

  var current = newFiber.alternate;

  if (current !== null) {
    var oldIndex = current.index;

    if (oldIndex < lastPlacedIndex) {
      // This is a move.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      // This item can stay in place.
      return oldIndex;
    }
  } else {
    // This is an insertion.
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}

提交阶段代码(移动和新增,不包含删除)

function commitReconciliationEffects(finishedWork) {
  
  var flags = finishedWork.flags;

  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    } 

    finishedWork.flags &= ~Placement;
  }

  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}
function commitPlacement(finishedWork) {
  var parentFiber = getHostParentFiber(finishedWork); 

  switch (parentFiber.tag) {
    case HostComponent://节点类型Fiber
      {
        var parent = parentFiber.stateNode;

        if (parentFiber.flags & ContentReset) {
          resetTextContent(parent);

          parentFiber.flags &= ~ContentReset;
        }
        //getHostSibling(A),找A后面的节点,跳过带有Placement标记的,第一个没有标记的就是要找的。
        //A是当前节点,是一个有Placement标记的节点,代表它是新增的,或者移动的。
        var before = getHostSibling(finishedWork); 
        //insertBefore(A的DOM,B的DOM),操作的是DOM,移动A,B在原位。
        insertOrAppendPlacementNode(finishedWork, before, parent);
        break;
      }

    case HostRoot:
    case HostPortal:
      {
        var _parent = parentFiber.stateNode.containerInfo;

        var _before = getHostSibling(finishedWork);

        insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
        break;
      }

    default:
      throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.');
  }
}
function getHostSibling(fiber) {

  var node = fiber;

  siblings: while (true) {
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
      
        return null;
      }

      node = node.return;
    }

    node.sibling.return = node.return;
    node = node.sibling;

    while (node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedFragment) {
   
      if (node.flags & Placement) {
        continue siblings;
      } 

      if (node.child === null || node.tag === HostPortal) {
        continue siblings;
      } else {
        node.child.return = node;
        node = node.child;
      }
    } 
    
    if (!(node.flags & Placement)) {
      // ⚠️Found it!
      return node.stateNode;
    }
  }
}
function insertOrAppendPlacementNode(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;

  if (isHost) {
    var stateNode = node.stateNode;

    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) ; else {
    var child = node.child;

    if (child !== null) {
      insertOrAppendPlacementNode(child, before, parent);
      var sibling = child.sibling;

      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
} 
function insertBefore(parentInstance, child, beforeChild) {
  parentInstance.insertBefore(child, beforeChild);
}

结语

创建wip(协调阶段)--> 打上标记(协调阶段)--> 操作DOM(提交阶段)。