2021-06-30

Fiber 树的构建

我们先来看一个简单的 demo:

import * as React from 'react';import * as ReactDOM from 'react-dom';class App extends React.Component { render() {  return (   <div className="container">    <div className="section">     <h1>This is the title.</h1>     <p>This is the first paragraph.</p>     <p>This is the second paragraph.</p>    </div>   </div>  ); }}ReactDOM.render(<App />, document.getElementById('root'));

首次渲染的调用栈如下图

file

以 performSyncWorkOnRoot 和 commitRoot 两个方法为界限,可以把 ReactDOM.render 分为三个阶段:

  1. Init
  2. Render
  3. Commit

Init Phase

render

很简单,直接调用 legacyRenderSubtreeIntoContainer。

export function render( element: React$Element<any>, container: Container, callback: ?Function,) { // 省略对 container 的校验逻辑 return legacyRenderSubtreeIntoContainer( null, element, container, false, callback, );}

这里需要注意一点,此时的 element 已经不是 render 中传入的 了,而是经过 React.createElement 转换后的一个 ReactElement 对象。

legacyRenderSubtreeIntoContainer

在这里我们可以看到方法取名的重要性,一个好的方法名可以让你一眼就看出这个方法的作用。legacyRenderSubtreeIntoContainer,顾名思义,这是一个遗留的方法,作用是渲染子树并将其挂载到 container 上。再来看一下入参,children 和 container 分别是之前传入 render 方法的 App 元素和 id 为 root 的 DOM 元素,所以可以看出这个方法会根据 App 元素生成对应的 DOM 树,并将其挂在到 root 元素上。

function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function,) { let root: RootType = (container._reactRootContainer: any); let fiberRoot; if (!root) { root = container._reactRootContainer = legacyCreateRootFromDOMContainer(  container,  forceHydrate, ); fiberRoot = root._internalRoot;	// 省略对 callback 的处理逻辑 unbatchedUpdates(() => {  updateContainer(children, fiberRoot, parentComponent, callback); }); } else { // 省略 else 逻辑 } return getPublicRootInstance(fiberRoot);}

下面来细看一下这个方法:

  1. 首次挂载时,会通过 legacyCreateRootFromDOMContainer 方法创建 container.reactRootContainer 对象并赋值给 root。 container 对象现在长这样:

file

  1. 初始化 fiberRoot 为 root.internalRoot,类型为 FiberRootNode。fiberRoot 有一个极其重要的 current 属性,类型为 FiberNode,而 FiberNode 为 Fiber 节点的对应的类型。所以说 current 对象是一个 Fiber 节点,不仅如此,它还是我们要构造的 Fiber 树的头节点,我们称它为 rootFiber。到目前为止,我们可以得到下图的指向关系:

file

  1. 将 fiberRoot 以及其它参数传入 updateContainer 形成回调函数,将回调函数传入 unbatchedUpdates 并调用。

unbatchedUpdates

主要逻辑就是调用回调函数 fn,也就是之前传入的 updateContainer。

export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R { const prevExecutionContext = executionContext; executionContext &= ~BatchedContext; executionContext |= LegacyUnbatchedContext; try {	// fn 为之前传入的 updateContainer return fn(a); } finally { executionContext = prevExecutionContext; if (executionContext === NoContext) {  resetRenderTimer();  flushSyncCallbackQueue(); } }}

updateContainer

updateContainer 方法做的还是一些杂活,我们简单总结一下:

  1. 计算当前 Fiber 节点的 lane(优先级)。
  2. 根据 lane(优先级),创建当前 Fiber 节点的 update 对象,并将其入队。
  3. 调度当前 Fiber 节点(rootFiber)。
export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function,): Lane { const current = container.current; const eventTime = requestEventTime(); // 计算当前节点的 lane(优先级) const lane = requestUpdateLane(current); if (enableSchedulingProfiler) { markRenderScheduled(lane); } const context = getContextForSubtree(parentComponent); if (container.context === null) { container.context = context; } else { container.pendingContext = context; } // 根据 lane(优先级)计算当前节点的 update 对象 const update = createUpdate(eventTime, lane); update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null) { update.callback = callback; } // 将 update 对象入队 enqueueUpdate(current, update); // 调度当前 Fiber节点(rootFiber) scheduleUpdateOnFiber(current, lane, eventTime); return lane;}

scheduleUpdateOnFiber

接着会进入 scheduleUpdateOnFiber 方法,根据 lane(优先级)等于 SyncLane,代码最终会执行 performSyncWorkOnRoot 方法。performSyncWorkOnRoot 翻译过来,就是指执行根节点(rootFiber)的同步任务,所以 ReactDOM.render 的首次渲染其实是一个同步的过程。

file

到这里大家可能会有个疑问,为什么 ReactDOM.render 触发的首次渲染是一个同步的过程呢?不是说在新的 Fiber 架构下,render 阶段是一个可打断的异步过程。
我们先来看看 lane 是怎么计算得到的,相关逻辑在 updateContainer 中的 requestUpdateLane 方法里:

export function requestUpdateLane(fiber: Fiber): Lane { const mode = fiber.mode; if ((mode & BlockingMode) === NoMode) { return (SyncLane: Lane); } else if ((mode & ConcurrentMode) === NoMode) { return getCurrentPriorityLevel() === ImmediateSchedulerPriority  ? (SyncLane: Lane)  : (SyncBatchedLane: Lane); } else if ( !deferRenderPhaseUpdateToNextBatch && (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes ) { return pickArbitraryLane(workInProgressRootRenderLanes); } // 省略非核心代码}

可以看出 lane 的计算是由当前 Fiber 节点(rootFiber)的 mode 属性决定的,这里的 mode 属性其实指的就是当前 Fiber 节点的渲染模式,而 rootFiber 的 mode 属性其实最终是由 React 的启动方式决定的。
React 其实有三种启动模式:

  • Legacy Mode: ReactDOM.render(<App />, rootNode)。这是目前 React App 使用的方式,当前没有删除这个模式的计划,但是这个模式不支持一些新的功能。
  • Blocking Mode:ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中,作为迁移到 concurrent 模式的第一个步骤。
  • Concurrent Mode: ReactDOM.createRoot(rootNode).render(<App />)。目前正在实验中,在未来稳定之后,将作为 React 的默认启动方式。此模式启用所有新功能。

因此不同的渲染模式在挂载阶段的差异,本质上来说并不是工作流的差异(其工作流涉及 初始化 → render → commit 这 3 个步骤),而是 mode 属性的差异。mode 属性决定着这个工作流是一气呵成(同步)的,还是分片执行(异步)的。

Render Phase

performSyncWorkOnRoot

核心是调用 renderRootSync 方法

renderRootSync

有两个核心方法 prepareFreshStack 和 workLoopSync,下面来逐个分析。

prepareFreshStack

首先调用 prepareFreshStack 方法,prepareFreshStack 中有一个重要的方法 createWorkInProgress。

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) {	// 通过 current 创建 workInProgress workInProgress = createFiber(  current.tag,  pendingProps,  current.key,  current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode;	// 使 workInProgress 与 current 通过 alternate 相互指向 workInProgress.alternate = current; current.alternate = workInProgress; } else {	// 省略 else 逻辑 } // 省略对 workInProgress 属性的处理逻辑 return workInProgress;}

下面我们来看一下 workInProgress 究竟是什么?workInProgress 是 createFiber 的返回值,接着来看一下 createFiber。

const createFiber = function( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,): Fiber { return new FiberNode(tag, pendingProps, key, mode);};

可以看出 createFiber 其实就是在创建一个 Fiber 节点。所以说 workInProgress 其实就是一个 Fiber 节点。
从 createWorkInProgress 中,我们还可以看出:

  1. workInProgress 节点是 current 节点(rootFiber)的一个副本。
  2. workInProgress 节点与 current 节点(rootFiber)通过 alternate 属性相互指向。

所以到现在为止,我们的 Fiber 树如下:

file

workLoopSync

接下来调用 workLoopSync 方法,代码很简单,若 workInProgress 不为空,调用 performUnitOfWork 处理 workInProgress 节点。

function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); }}

performUnitOfWork

performUnitOfWork 有两个重要的方法 beginWork 和 completeUnitOfWork,在 Fiber 的构建过程中,我们只需重点关注 beginWork 这个方法。

function performUnitOfWork(unitOfWork: Fiber): void { const current = unitOfWork.alternate; setCurrentDebugFiberInDEV(unitOfWork); let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, subtreeRenderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { next = beginWork(current, unitOfWork, subtreeRenderLanes); } resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { completeUnitOfWork(unitOfWork); } else { workInProgress = next; } ReactCurrentOwner.current = null;}

目前我们只能看出,它会对当前的 workInProgress 节点进行处理,至于怎么处理的,当我们解析完 beginWork 方法再来总结 performUnitOfWork 的作用。

beginWork

根据 workInProgress 节点的 tag 进行逻辑分发。tag 属性代表的是当前 Fiber 节点的类型,常见的有下面几种:

  • FunctionComponent:函数组件(包括 Hooks)
  • ClassComponent:类组件
  • HostRoot:Fiber 树根节点
  • HostComponent:DOM 元素
  • HostText:文本节点
function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes,): Fiber | null { // 省略非核心(针对树构建)逻辑 switch (workInProgress.tag) {	// 省略部分 case 逻辑	// 函数组件(包括 Hooks) case FunctionComponent: {  const Component = workInProgress.type;  const unresolvedProps = workInProgress.pendingProps;  const resolvedProps =  workInProgress.elementType === Component   ? unresolvedProps   : resolveDefaultProps(Component, unresolvedProps);  return updateFunctionComponent(  current,  workInProgress,  Component,  resolvedProps,  renderLanes,  ); }	// 类组件 case ClassComponent: {  const Component = workInProgress.type;  const unresolvedProps = workInProgress.pendingProps;  const resolvedProps =  workInProgress.elementType === Component   ? unresolvedProps   : resolveDefaultProps(Component, unresolvedProps);  return updateClassComponent(  current,  workInProgress,  Component,  resolvedProps,  renderLanes,  ); }	// 根节点 case HostRoot:  return updateHostRoot(current, workInProgress, renderLanes);	// DOM 元素 case HostComponent:  return updateHostComponent(current, workInProgress, renderLanes);	// 文本节点 case HostText:  return updateHostText(current, workInProgress);	// 省略部分 case 逻辑 } // 省略匹配不上的错误处理}

当前的 workInProgress 节点为 rootFiber,tag 对应为 HostRoot,会调用 updateHostRoot 方法。

file

rootFiber 的 tag(HostRoot)是什么来的?核心代码如下:

export function createHostRootFiber(tag: RootTag): Fiber { // 省略非核心代码 return createFiber(HostRoot, null, null, mode);}

在创建 rootFiber 节点的时候,直接指定了 tag 参数为 HostRoot。

updateHostRoot

updateHostRoot 的主要逻辑如下:

  1. 调用 reconcileChildren 方法创建 workInProgress.child。
  2. 返回 workInProgress.child。
function updateHostRoot(current, workInProgress, renderLanes) {	// 省略非核心逻辑 if (root.hydrate && enterHydrationState(workInProgress)) { 	// 省略 if 成立的逻辑 } else { reconcileChildren(current, workInProgress, nextChildren, renderLanes); resetHydrationState(); } return workInProgress.child;}

这里有一点需要注意,通过查看源码,你会发现不仅是 updateHostRoot 方法,所以的更新方法最终都会调用下面这个方法:

reconcileChildren(current, workInProgress, nextChildren, renderLanes);

只是针对不同的节点类型,会有一些不同的处理,最终殊途同归。

reconcileChildren

reconcileChildren 根据 current 是否为空进行逻辑分发。

export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes,) { if (current === null) { workInProgress.child = mountChildFibers(  workInProgress,  null,  nextChildren,  renderLanes, ); } else { workInProgress.child = reconcileChildFibers(  workInProgress,  current.child,  nextChildren,  renderLanes, ); }}

此时 current 节点不为空,会走 else 逻辑,调用 reconcileChildFibers 创建 workInProgress.child 对象。

reconcileChildFibers

根据 newChild 的类型进行不同的逻辑处理。

function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, lanes: Lanes, ): Fiber | null {	// 省略非核心代码 const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) {  switch (newChild.$$typeof) {  case REACT_ELEMENT_TYPE:   return placeSingleChild(   reconcileSingleElement(    returnFiber,    currentFirstChild,    newChild,    lanes,   ),   );	 // 省略其他 case 逻辑  } }	// 省略非核心代码 if (isArray(newChild)) {  return reconcileChildrenArray(  returnFiber,  currentFirstChild,  newChild,  lanes,  ); }	// 省略非核心代码 }

newChild 很关键,我们先明确一下 newChild 究竟是什么?通过层层向上寻找,你会在 updateHostRoot 方法中发现它其实是最开始传入 render 方法的 App 元素,它在 updateHostRoot 中被叫做 nextChildren,到这里我们可以做出这样的猜想,rootFiber 的下一个是 App 节点,并且 App 节点是由 App 元素生成的,下面来看一下 newChild 的结构:

file

可以看出 newChild 类型为 object,$$typeof 属性为 REACT_ELEMENT_TYPE,所以会调用:

placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, lanes, ),);
reconcileSingleElement

下面继续看 reconcileSingleElement 这个方法:

function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, lanes: Lanes,): Fiber { const key = element.key; let child = currentFirstChild; // 省略 child 不存在的处理逻辑 if (element.type === REACT_FRAGMENT_TYPE) {	// 省略 if 成立的处理逻辑 } else { const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; }}

方法的调用比较深,我们先明确一下入参,returnFiber 为 workInProgress 节点,element 其实就是传入的 newChild,也就是 App 元素,所以这个方法的作用为:

  1. 调用 createFiberFromElement 方法根据 App 元素创建 App 节点。
  2. 将新生成的 App 节点的 return 属性指向当前 workInProgress 节点(rootFiber)。此时 Fiber 树如下图:

file

  1. 返回 App 节点。
placeSingleChild

接下来调用 placeSingleChild:

function placeSingleChild(newFiber: Fiber): Fiber { if (shouldTrackSideEffects && newFiber.alternate === null) { newFiber.flags = Placement; } return newFiber;}

入参为之前创建的 App 节点,它的作用为:

  1. 当前的 App 节点打上一个 Placement 的 flags,表示新增这个节点。
  2. 返回 App 节点。

之后 App 节点会被一路返回到的 reconcileChildren 方法:

workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes,);

此时 workInProgress 节点的 child 属性会指向 App 节点。此时 Fiber 树为:

file

beginWork 小结

beginWork 的链路比较长,我们来梳理一下:

  1. 根据 workInProgress.tag 进行逻辑分发,调用形如 updateHostRoot、updateClassComponent 等更新方法。
  2. 所有的更新方法最终都会调用 reconcileChildren,reconcileChildren 根据 current 进行简单的逻辑分发。
  3. 之后会调用 mountChildFibers/reconcileChildFibers 方法,它们的作用是根据 ReactElement 对象生成 Fiber 节点,并打上相应的 flags,表示这个节点是新增,删除还是更新等等。
  4. 最终返回新创建的 Fiber 节点。

简单来说就是创建新的 Fiber 字节点,并将其挂载到 Fiber 树上,最后返回新创建的子节点。

performUnitOfWork 小结

下面我们来小结一下 performUnitOfWork 这个方法,先来回顾一下 workLoopSync 方法。

function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); }}

它会循环执行 performUnitOfWork,而 performUnitOfWork,我们已经知道它会通过 beginWork 创建新的 Fiber 节点。它还有另外一个作用,那就是把 workInProgress 更新为新创建的 Fiber 节点,相关逻辑如下:

// 省略非核心代码// beginWork 返回新创建的 Fiber 节点并赋值给 nextnext = beginWork(current, unitOfWork, subtreeRenderLanes);// 省略非核心代码if (next === null) { completeUnitOfWork(unitOfWork);} else { // 若 Fiber 节点不为空则将 workInProgress 更新为新创建的 Fiber 节点 workInProgress = next;}

所以当 performUnitOfWork 执行完,当前的 workInProgress 都存储着下次要处理的 Fiber 节点,为下一次的 workLoopSync 做准备。
performUnitOfWork 作用总结如下:

  1. 通过调用 beginWork 创建新的 Fiber 节点,并将其挂载到 Fiber 树上
  2. 将 workInProgress 更新为新创建的 Fiber 节点。

App 节点的处理

rootFiber 节点处理完成之后,对应的 Fiber 树如下:

file

接下......

原文转载:http://www.shaoqun.com/a/837138.html

跨境电商:https://www.ikjzd.com/

reverb:https://www.ikjzd.com/w/1273

抢注商标:https://www.ikjzd.com/w/1053

trademanager:https://www.ikjzd.com/w/730


我们先来看一个简单的demo:import*asReactfrom'react';import*asReactDOMfrom'react-dom';classAppextendsReact.Component{render(){return(<divclassName="container"><divclassName=&quo
e邮宝:https://www.ikjzd.com/w/594.html?source=tagwish
Wish:英法德意西为主的欧洲地区GMV占比已超过美国!:https://www.ikjzd.com/articles/100215
亚马逊Listing怎么写?亚马逊Listing优化技巧分享!:https://www.ikjzd.com/articles/100216
亚马逊社交推广怎么做?旺季就靠它了!:https://www.ikjzd.com/articles/100217
惊!641个集装箱从菲律宾马尼拉港离奇消失!原因竟是...:https://www.ikjzd.com/articles/100218
宝宝腿张大点进不去 宝宝我们车上来一次:http://lady.shaoqun.com/a/256938.html
他把舌头伸进两腿之间 舌尖逗弄她颤抖的小核:http://lady.shaoqun.com/m/a/248221.html
情感故事:回忆我当坐台小姐的那些年(11/15):http://www.30bags.com/m/a/249663.html
女方晚上走了,失去联系6天。还是没有消息。强奸和谋杀被拘留了5天:http://lady.shaoqun.com/a/391626.html
苹果发布iOS 15 Beta 2测试版,新增功能、修复Bug,该更新吗?:https://www.ikjzd.com/articles/146227
情况恶化,美国FBA仓库爆仓严重,热门大仓预约难:https://www.ikjzd.com/articles/146226
亚马逊卖家请注意:美国人正在抢购这些产品:https://www.ikjzd.com/articles/146229

No comments:

Post a Comment