上篇以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