写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
本节的我们将从 上一节留下的问题出发,谈谈 beginWork () 中怎么样把一个 element 节点变成一个 fiber 结点,其中我们会提到我们的 beginwork 怎么样处理更新和首次渲染,通过什么样的方式来加速渲染,以及对于不同类型的节点使用怎么样不同的逻辑
beginWork
上一节中我们讲到,不论是什么类型的任务,最后都会调用 performUnitOfWork
的逻辑,其中使用了 beginWork 这个函数,它的操作很复杂,这一节我们从它开始讲起,它的代码在这个位置:
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js
它的源代码很长,我们需要一部分一部分来看,完整的源代码可以查看提供的链接,首先是它的整体架构,我们首先通过传入的 current
是不是空来判定本次渲染是首次挂载还是后续的更新,如果此处为首次渲染,didReceiveUpdate
设为 false 。注意,current !== null
这个判定将会在下面的内容中多次出现,现在可以记住它。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 判定 current 是不是空来断定是更新产生还是执行之前的任务
if (current !== null) {
//....
} else {
current = false;
//省略....
}
workInProgress.lanes = NoLanes;
// 根据 workInProgress 不同的tag,进行不同的处理
switch (workInProgress.tag) {
//case.....
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
之后我们来看 current
不是空的情况,也就是更新逻辑,我们获取前后两次的 props
,你可以会看一下 Fiber 这节,我们有一个属性是 memoizedProps
,缓存了上一次的 props , 当时可能大家都不明白为什么需要进行缓存,其实它在这里发挥了用处,通过对比前后两次的 props
可以快速判定是不是需要更新。
- 如果两次 props 不相等,标记为需要更新
- 如果两个 props 相等,并且没有 挂起的更新或上下文更改(这部分和调度与优先级有关,之后会展开),我们就提前退出,复用之前的节点即可
- 如果处于传统模式下(非分片模式,可以控制节点是不是强制更新,也就是不管变不变都更新它),判定当前节点是否要强制更新
if (current !== null) {
// 获取新旧的 props
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 如果 props 或 context 发生变化,将 Fiber 标记为需要更新
didReceiveUpdate = true;
} else {
// props和上下文都没有改变,检查是否有挂起的更新或上下文更改
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
// 若没有任何的更新,上下文也没发生变化,直接返回
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// 传统模式下,当前节点是否要强制更新
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
当我们把 didReceiveUpdate
处理完毕后,我们根据节点的类型进行不同的更新处理,这里的逻辑很复杂,基本上每一个 tag 都需要一个单独的处理,我们讲述一些常用的内容,完整的内容可以自行阅读源码或者查阅资料:
switch (workInProgress.tag) {
case IndeterminateComponent: {
//****
}
case LazyComponent: {
//****
}
case FunctionComponent: {
//函数组件 ****
}
case ClassComponent: {
//类组件 ****
}
case HostRoot:
// 初始render()时,只有两棵树的根节点,current和 workInProgress 分别指向到这两棵树的根节点
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
// html标签
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
//****
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
// 如 <React.StrictMode></React.StrictMode>
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
//****
}
case SimpleMemoComponent: {
//****
}
case IncompleteClassComponent: {
//****
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
//****
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
//****
}
case CacheComponent: {
//****
}
case TracingMarkerComponent: {
//****
}
}
HostRoot
当节点是 HostRoot 类型的时候,也就是初始化的时候,我们传入了我们的两棵树的根节点,我们这样处理:
- 首先我们获取传入本次的 props,之后我们获取上次的 State,你可以回看一下 Fiber 架构这一节,当时我们提到了其中有一些和 props 以及 state 相关的内容,其中
pendingProps
属性存放是本次渲染的 props ,这个会在节点生成的时候获取;而memoizedState
属性存缓存了上一次生成的 state,这个属性是为了我们这里的算法服务的,它可以通过比较上次和这次渲染的节点是不是一样来判定我们是不是需要提前终止生成的逻辑,从而提高渲染的效率 - 之后我们把 current 的 updateQueue 队列(其中存放了 element)复制给 workInProgress ,目的是防止再后续修改的时候相互影响(因为 js 的对象的引用传值)
- 通过本次传递的 props 和 workInProgress 中的 element 元素,我们可以使用
processUpdateQueue
将 element 添加到 Fiber 的 memoizedState 和 updateQueue 更新队列中 ,关于这个函数,我们会在进程调度的时候展开来讲,总之在执行完成这个函数后,我们可以通过memoizedState
拿到本次执行生成的 state,也就拿到本次的 element - 如果前后两次的 element 相同,那么我们可以复用之前的元素,否则我们调用
reconcileChildren
创建一个新的节点 - 注意:如果这次是首次渲染,那么 prevChildren 肯定是 null ,所以肯定需要调用我们的
reconcileChildren
来创建我们的 Fiber - 最后我们返回我们的
workInProgress.child
,也就是下一个节点,如果下一个节点不是空,那么继续运行整个逻辑
//case HostRoot: return updateHostRoot(current, workInProgress, renderLanes);
function updateHostRoot(current, workInProgress, renderLanes) {
//把一些有用的信息推入内部栈
pushHostRootContext(workInProgress);
if (current === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
//获取本次的props
const nextProps = workInProgress.pendingProps;
//获取上传的state
const prevState = workInProgress.memoizedState;
//获取上次的element
const prevChildren = prevState.element;
// 将current节点中的updateQueue中的属性,复制给workInProgress节点
cloneUpdateQueue(current, workInProgress);
// 主要的任务是将 Element 添加到 Fiber 的 memoizedState 和 updateQueue 更新队列中
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// 得到各种更新后的最新数据
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
//省略....
//获得本次的 element
const nextChildren = nextState.element;
if (supportsHydration && prevState.isDehydrated) {
//省略.....
} else {
resetHydrationState();
// 若前后两次的 element 如果没有变化,则提前退出,直接复用之前的节点
// 而初始时,prevChildren为null,nextChildren为将要更新的element,肯定不相等
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 执行 reconcileChildren 把 element 变成 fiber
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
FunctionComponent
我们继续看 switch 语句,如果传入的节点是 FunctionComponent
也就是函数组件,我们会执行这样的逻辑:
- 函数组件的主体就放在属性 type 中,我们获取其 type ,后续执行该 type() 即可。
- 然后我们调用
updateFunctionComponent
函数处理我们的函数组件
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,
);
}
之后我们来看看 updateFunctionComponent
:
- 这里调用了
renderWithHooks
得到了nextChildren
- 然后根据我们之前得到
didReceiveUpdate
判定是不是可以提前退出复用之前的节点 - 如果不能复用(需要更新或者首次),我们则调用
reconcileChildren
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
//....省略 dev 模式代码
//获取我们的 context 信息(不知道 context 可以自行查阅 react 文档)
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
// ....省略优先级调度
// 处理 HOOKS
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
//节点更新,但是状态和props没有改变
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
workInProgress.flags |= PerformedWork;
//调用更新
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
而在 renderWithHooks
这个函数中,我们根据是不是首次渲染来判定是更新我们的 hooks 还是 初始化我们的 hooks,这个我们后续会详细来说,之后我们直接执行我们的函数组件,传入它需要的 props 和 上下文信息,返回我们执行后创造出来的内部的 element 结构,这就是为什么我们的函数组件要 return 一个 jsx 的理由,它其实是在这里被作为一个函数执行,然后返回它的内部结构,我们将这个结构输入 reconcileChildren
生成我们的 Fiber
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
// 根据是否是初始化挂载,来决定是初始化hook,还是更新hook
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
// 传入函数,执行我们的函数组件
let children = Component(props, secondArg);
return children;
}
ClassComponent
当我们传入是类组件的时候,我们执行 updateClassComponent()
这个函数,它的处理比函数组件复杂一些,它的大致逻辑如下:
- 首先获取我们的上下文,然后我们会看 Fiber 中的定义,
stateNode
这个属性用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例,因为我们是 class 节点,那么这个属性就可以获取到我们的实例 ,不知道实例是什么的可以看看面向对象的概念 - 如果实例是空的,那么我们需要通过传入的组件创建出一个实例来与 workInProgress 绑定,
constructClassInstance
用于初始化节点,而mountClassInstance
则负责挂载 - 如果已经初始化,按照是不是首次渲染来判定我们是使用节点还是更新节点
- 最后我们使用
finishClassComponent
结束我们的类组件操作
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
// ... 省略 dev 模式代码
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
const instance = workInProgress.stateNode; // 在类组件中,stateNode存储的是类的实例
let shouldUpdate;
if (instance === null) {
//没有实例,初始化一个实例
resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
// 创建出一个实例,然后将 workInProgress 与这个实例绑定。
constructClassInstance(workInProgress, Component, nextProps);
// 挂载,执行 componentWillMount
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// current 的 fiber 节点为空,但 workInProgress.stateNode 经有这个类组件的实例了,可以直接使用
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// current的fiber节点不为空,判断 current 和 workInProgress 两者是否有变化
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
// 执行render()方法得到element,
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
// 省略 dev 模式代码 ....
return nextUnitOfWork;
}
我们还是首先来看初始化的部分,constructClassInstance
函数的逻辑大致是首先初始化一个实例,如何把 state 等信息挂载在上面,最后把 workInProgress 和初始化出来的实例绑定
function constructClassInstance(workInProgress: Fiber, ctor: any, props: any): any {
// 初始化一个
let instance = new ctor(props, context);
// 获取到类组件中的state,放到workInProgress中的memoizedState字段中
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined ? instance.state : null);
/**
* 将workInProgress和类的实例进行互相绑定
* instance.updater = workInProgress;
* workInProgress.stateNode = instance;
*/
adoptClassInstance(workInProgress, instance);
return instance;
}
之后我们执行 mountClassInstance
这个函数,组件中的部分生命周期在这里运行:
- 我们首先获取上下文和 state 以及 props
- 通过 props 和 state 我们计算出新的 state 更新给节点
- 然后依次执行
componentWillMount
生命周期,并且设置componentDidMount
的优先级信息,但是并不在这个函数执行
// 执行渲染之前的一些生命周期函数
function mountClassInstance(workInProgress: Fiber, ctor: any, newProps: any, renderLanes: Lanes): void {
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;
// 初始化更新链表
initializeUpdateQueue(workInProgress);
// 获取上下文
const contextType = ctor.contextType;
if (typeof contextType === 'object' && contextType !== null) {
instance.context = readContext(contextType);
} else if (disableLegacyContext) {
instance.context = emptyContextObject;
} else {
const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
instance.context = getMaskedContext(workInProgress, unmaskedContext);
}
instance.state = workInProgress.memoizedState;
// 这是一个官方的 API 用于在 render 之前调用,它传入 props 和 state,返回一个对象来更新我们的 state(加入了 props 的内容)
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
instance.state = workInProgress.memoizedState;
}
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function')
) {
// 执行 componentWillMount 和 UNSAFE_componentWillMount
callComponentWillMount(workInProgress, instance);
// 如果是更新节点,执行更新操作,这个后续会讲到
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
//设置 componentDidMount 的优先级信息(并不在此时运行)
if (typeof instance.componentDidMount === 'function') {
let fiberFlags: Flags = Update;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStatic;
}
workInProgress.flags |= fiberFlags;
}
}
最后我们来看看 finishClassComponent
,它的逻辑就很简单了,它调用了类组件的 render
方法获取了对应的 element 元素,之后还是调用了 reconcileChildren
方法
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
const instance = workInProgress.stateNode;
// 使用 render() 方法获取 jsx 对应的 element 结构
nextChildren = instance.render();
// 调用函数 reconcileChildren()
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
workInProgress.memoizedState = instance.state;
return workInProgress.child;
}
HostComponent 和 HostText
这个类型对应的是原生的 HTML ,它调用了 updateHostComponent
这个处理方法,它仅仅是多了一个逻辑判定是不是文本节点,这个逻辑在 shouldSetTextContent
中:
function updateHostComponent(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
pushHostContext(workInProgress);
//首次渲染进行 hydrate 的操作,也就是复用原本存在的root的内部的DOM节点。后续提到 hydrate
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
// 判断该节点是否是文本节点, 即里面不再嵌套其他类型的节点
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// 文本节点的话,没有子节点
nextChildren = null;
}
// 如果是更新一个节点,它之前是文本节点,但是现在不是文本节点
else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// 设置 ContentReset,表示文本节点重置
workInProgress.flags |= ContentReset;
}
// 获取 Component 的 DOM 实例,更新 ref
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
//判断是不是文本节点的逻辑
function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'option' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}
updateHostText
用于处理文本,对应的类别是 HostText ,它的处理类似上面文本节点的处理:先调用 tryToClaimNextHydratableInstance
然后直接返回 null (因为不可能有孩子了)
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
return null;
}
IndeterminateComponent
这个有一个例外,就是我们的 FunctionComponent,在第一次识别的时候会被认为是 IndeterminateComponent ,因为可能有以下几种特殊的函数组件,他们是函数组件,但是写法又可能近似于类组件,所以我们需要特殊的处理来判别他们的类型:
// function中 return 带有 render() 的obj
function App() {
return {
render() {
return <p>function render</p>;
},
};
}
// render() 在 函数 App() 的prototype上
function App() {}
App.prototype.render = () => {
return <p>function prototype render</p>;
};
// 继承React.Component
function App() {
return {
componentDidMount() {
console.log('componentDidMount');
},
render() {
return <p>function render</p>;
},
};
}
App.prototype = React.Component.prototype; // 或 new React.Component()
// function 中直接return一个jsx
function App() {
return <p>function jsx</p>;
}
这个处理在 mountIndeterminateComponent
中进行,它通过判定返回的 value 的类型
function mountIndeterminateComponent(
_current, // 好奇怪,这里为什么要用下划线开头
workInProgress,
Component,
renderLanes,
) {
// 省略....
let value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
if (
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// 类组件
workInProgress.tag = ClassComponent;
} else {
// 函数组件
workInProgress.tag = FunctionComponent;
}
}
总结
这一节中,我们通过解析 beginWork
函数,明白了我们是怎么样把一个 element 节点变成一个 fiber 结点的:
- 它首先通过比较两次的
props
判定本次任务是更新还是首次渲染,需不需要任务是不是需要更新,还是可以复用之前的节点 - 之后根据我们传入节点的类型分别处理他们:
- 如果你传入一个
HostRoot
, 那么它通过比较两次传入的 element 判定是不是需要更新,需要更新则传入reconcileChildren
- 如果你传入一个
FunctionComponent
,那么我们先处理它的 hooks,然后传入 props 和上下文来调用这个函数,返回 element 元素传入reconcileChildren
- 如果你传入一个
ClassComponent
,那么我们需要先初始化一个这个 class 的实例挂载到 fiber 上,然后处理它的 props 生成 state,再处理它的componentWillMount
生命周期 ,最后调用render
方法获取它的 element ,传入reconcileChildren
- 如果你传入一个原生的 HTML 节点,它会判定它是不是文本节点,如果是则停止后续的处理,否则传入
reconcileChildren
;如果你传入的是文本节点,那么直接停止后续的处理 - 有一种特殊情况是,因为 React 中的函数组件有多种写法,有些形似函数组件的也可能是类组件,为了保险起见,函数组件会先被设定为
IndeterminateComponent
,然后在对这个类型的处理中,会识别你写的组件是函数组件还是类组件
在这一节中,我们只是挑选了一些覆盖大部分情况的 tag 来讲解,如果你想了解其他特殊的 tag 的处理可以自行阅读源码或者查看其他大神的教程。在经过了这一节后,我们的整个渲染流程只剩下了最后一步,那就是 reconcileChildren
这个函数,调用它也预示着我们进入了 React 渲染流程的 reconcile
阶段,下一节我们将讲解它,而 reconcile
中我们会遇到 React 最经典的 DIFF 算法,这也是 React 的精髓所在,它帮助虚拟 DOM 高效的渲染更新,欢迎大家持续关注专栏。
参考
https://www.xiabingbao.com/post/react/react-beginwork-riew9h.html
https://zhuanlan.zhihu.com/p/115533068