React源码揭秘 | 不同类型Fiber的BeginWork流程

上篇以BeginWork阶段处理HostRoot根Fiber节点为例介绍了的child reconcile协调过程,这篇主要看一下各种类型的Fiber节点在BeginWork中的处理流程。

BeginWork是一个分流处理的流程,不同类型的Fiber根据其tag区分,调用不同的处理函数,如下图所示:

/** 递的过程 */
export function beginWork(wip: FiberNode, renderLane: Lane): FiberNode | null {
  // 比较,当前的fiber 和 旧的fiber
  switch (wip.tag) {
    case Fragment:
      return updateFragment(wip);
    case MemoComponent:
      return updateMemoComponent(wip, renderLane);
    case HostRoot:
      return updateHostRoot(wip, renderLane);
    case HostComponent:
      return updateHostComponent(wip);
    case HostText:
      return null;
    case FunctionComponent:
      return updateFunctionComponent(wip, wip.type as Function, renderLane);
    default:
      console.warn("beginWork未实现的类型", wip.tag);
      break;
  }
  return null;
}

updateHostRoot - 更新根节点

上篇介绍了HostRoot的处理流程,复习一下,其源码如下:

/** 处理HostRoot节点的比较 */
function updateHostRoot(wip: FiberNode, renderLane: Lane): FiberNode {
  /** 获取updateQueue */
  const updateQueue = wip.updateQueue;
  /** 这里hostRoot的update由updateContainer放入,其对应的action就是其element */
  const { memorizedState: newChildren } = updateQueue.process(renderLane);
  /** 协调其子节点 */
  reconcileChildren(wip, newChildren);

  /** 返回下一个待处理节点 即wip.child */
  return wip.child;
}

在入口调用React.createRoot(DOM Container).render(<App/>)时,render函数调用updateContainer函数讲<App/>的ReactElement元素放到HostRoot节点的updateQueue中

/**
 * 更新container 需要传入
 * @param element 新的element节点
 * @param root APP根节点
 */
const updateContainer = (element: ReactElement, root: FiberRootNode) => {
  // 默认情况下 同步渲染
  scheduler.runWithPriority(PriorityLevel.IMMEDIATE_PRIORITY, () => {
    // 请求获得当前更新lane
    const lane = requestUpdateLane();
    // 获hostRootFiber
    const hostRootFiber = root.current;
    // 更新的Element元素入队
    hostRootFiber.updateQueue?.enqueue(
      new Update<ReactElement>(element, lane),
      hostRootFiber,
      lane
    );
    // scheduleUpdateOnFiber 调度更新
    scheduleUpdateOnFiber(root.current, lane);
  });
};

目前可以简单的把updateQueue理解为一个队列

updateQueue内的元素为Update对象,其包含一个action属性,这个属性可以为任意值

enqueue方法就是把一个Update对象推入队列

process方法就是依次从头遍历队列,对于每个Update的action,如果:

action是函数类型,即typeof action === 'function' 就把前一次的值作为参数传入并调用 获取返回值保存。

action非函数,则把这个action的值保存,继续调用下一个Update.action

最后返回结果。

[updateQueue其实很复杂,是一个包含lane优先级计算的环形链表,这里你只需要先知道其功能即可!]

updateContainer执行之后,其updateQueue为如下状态

 即,把App的ReactElement元素保存在HostRootFiber的UpdateQueue中。

在renderRoot的中的prepareRefreshStack调用createWorkInProgress创建"对称的" alternate HostRootFiber根节点时,也会吧UpdateQueue一同拷贝过去:

 即,初始化之后的Fiber状态如下:

此时,在BeginWork调用updateHostRoot时 wip.updateQueue.process() 即可获得<App/>的ReactElement元素。

获得新的ReactElement元素newChildren后,将wip和newChildren传入reconcileChild创建子FIber节点,并且给wip.child赋值,最后返回wip.child节点,完成处理!

updateHostComponent - 更新宿主节点

HostComponent就是浏览器宿主提供的节点,比如 div元素,span, a 这些原生就有的元素。

当使用createElement(type,props,children) type为字符串的时候,此时创建的就是HostComponent元素。

处理HostComponent的逻辑更简单,本质就是拿到newChild的element元素,把wip和newChildren传入reconcileChildren,返回wip.child 即可,源码如下:


/** 处理普通节点的比较 */
function updateHostComponent(wip: FiberNode): FiberNode {
  /** 1.获取element.children */
  const hostChildren = wip.pendingProps?.children;
  /** 2. 协调子元素 */
  reconcileChildren(wip, hostChildren);
  /** 3.返回第一个child */
  return wip.child;
}

注意,hostComponent的子元素从props.children中获取,这里和HostRoot不太一样 

HostText-文本节点的更新

对于文本节点,在ChildReconciler.ts/reconcileTextNodeFiber中,会调用

new FiberNode(HostText,{content}) 完成Fiber节点的创建

但是由于文本节点一定没有子节点,即文本节点就是叶子节点,执行到此说明递的过程结束,需要转换到"归" 的过程,此时直接return null 转而执行completeWork即可!

updateFragment - 更新Fragment节点

 这里先解答一个疑惑啊,为什么React一个组件不能返回多个元素,比如,你不能

function Comp(){
  return <div>Hello</div><div>World</div>
}

但是你可以返回一个数组,如:

function Comp(){
  return [<div>Hello</div>,<div>World</div>]
}

或者使用一个Fragment(或其语法糖<></>)包装

function Comp(){
  return <Fragment>
            <div>Hello</div
            <div>World</div>
        </Fragment>
}

这样做的原因,仅仅是因为,在babel转换JSX的时候,需要把标签转换成createElemnent的形式。

对于一个函数返回多个div的情况,会转换成多个return的形式,这在js的语法中是不允许的

function App(){
    return createElement('div',{},'hello') , createElement('div',{},'world')
   // ?? 语法错误 ❌
}

但是为什么Fragment就可以? 因为Fragment多添加了一层Element元素,babel会把夹在元素中的多个Element元素转换成ReactElement的数组,这样就成了:

function App(){
    return createElement(REACT_FRAGMENT_TYPE,{},[
        createElement('div',{},'hello'),
        createElement('div',{},'world')
    ])
   // 符合语法要求 ✅
}

数组也可以 ,因为也不违反语法

function App(){
    return [
        createElement('div',{},'hello'),
        createElement('div',{},'world')
    ]
   // 符合语法要求 ✅
}

 函数直接返回之所以不行,是因为babel转换的时候也不知道你这个函数会用在哪里,直接返回两个标签无法像Fragment一样包装处理.

 Fragment的除了children没有其他的属性,所以其pendingProps就是其children

updateFragment获得其children,并且和wip一起传入reconcileChildren,并且返回wip.child如下:

/** 处理Fragment */
function updateFragment(wip: FiberNode): FiberNode {
  /** fragment的pendingProps就是children */
  const nextChildElement = wip.pendingProps as ReactElementChildren;
  reconcileChildren(wip, nextChildElement);
  return wip.child;
}

updateFunctionComponent - 更新函数节点

函数节点和上面介绍的节点区别就是,函数节点需要运行一下其type保存的函数才能获得newChild 其实现如下:

/** 处理函数节点的比较 */
function updateFunctionComponent(
  wip: FiberNode,
  Component: Function,
  renderLane: Lane
): FiberNode {
  const nextChildElement = Component(wip.pendingProps);
  reconcileChildren(wip, nextChildElement);
  return wip.child;
}

其中,Component就是wip.type 运行Component函数 传入props,得到newChild ,其他和上面一样了。

当然,这里只是最简单的实现,真正的functionComponent需要处理hooks的逻辑,其实现在renderWithHooks这个函数里,这里先不讲,后面说!

 最后还剩下一个UpdateMemoComponent没说,这个放在后面和bailout逻辑一起讲!

以上我们就实现了beginWork 即render阶段“递”的过程,下一篇我们讲一下"归"的阶段,即completeWork 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值