引言
在React 源码解读之首次渲染流程中我们讲到了 React 在首次渲染过程(其实更新过程也一样)中存在 Render 和 Commit 两大阶段,其中 Render 阶段又可称为协调阶段,它包括 beginWork 和 completeWork,本文着重讲讲 beginWork。
beginWork
将 beginWork 进行简化后,我们发现该函数可以分为两大部分,以 workInProgress.lanes = NoLanes; 为分界线,前面部分是关于复用 Fiber 节点的逻辑,即进入 bailout 流程,后面部分是关于更新当前 Fiber 节点的逻辑。
1 | function beginWork( |
这里的第一个问题是:bailout 是在干什么,有什么意义?
bailout 的意义
在React 源码解读之首次渲染流程中,我们通过一个动画分析了 React 首次渲染的 Render 过程,在 beginWork 阶段会重新构建一颗 Fiber 树,但是当 命中 bailout 逻辑且子孙节点没有更新任务时,会复用以当前 Fiber 节点为根的整颗子树。
1 |
|
这里要注意的是是否返回 null 还要看当前 Fiber 节点的子孙节点们中是否有更新任务,如果有则不能直接返回 null,仍然需要对子节点进行处理。这里有个问题,当前节点是怎么知道子孙节点是否有更新任务的?答案是因为当某个节点触发了更新时,会沿着 Fiber 一直往上冒泡,这个过程中每个节点都能收集到自己子孙节点的相关信息:
1 | function markUpdateLaneFromFiberToRoot( |
接下来就是第二个问题了,什么时候会进入 bailout?
bailout 条件
从代码中我们可以知道 bailout 的前提是:
oldProps === newPropshasLegacyContextChanged()为falseincludesSomeLane(renderLanes, updateLanes)为false
注意,因为我们只考虑生产环境,所以这里忽略 (__DEV__ ? workInProgress.type !== current.type : false),下面来分别分析一下这三种情况:
oldProps === newProps
我们通过一个例子来分析一下,下面例子中当 App 触发更新时 Son 对应的 Fiber 节点能复用吗?
1 | import React from 'react' |
答案是不能。因为 return <Son /> 实际是上为转换为 return React.createElement(Son)。两次 render 函数返回的对象完全不同,故这里 oldProps !== newProps:

若想复用的话,可以这样写:
1 | import React from 'react' |
这样,每次 render 返回的都是同一个 ReactElement 对象,通过其创建的 Fiber 上的 pendingProps 和 memoizedProps 也都指向同一个对象:

hasLegacyContextChanged 为 true
这个判断条件是留给已废弃的 context 使用的:
1 | class Son extends React.Component { |
这里我们跟前面一样缓存了 <Son />,但是由于使用了旧的已废弃的 Context,hasLegacyContextChanged() 会为 true,所以这个例子不会走 bailout。关于 Context,这里还有很多可以展开的内容,这个留给以后单独写一篇 Context 的文章吧。
includesSomeLane(renderLanes, updateLanes) 为 false
includesSomeLane(renderLanes, updateLanes) 这句代码是为了判断当前节点上的更新任务的优先级是否包含在了此次更新
的优先级之中。如果当前节点的更新优先级大于等于此次更新的优先级,则 includesSomeLane(renderLanes, updateLanes) 会返回 true。
1 | import React from 'react' |
上面的例子当点击 div 时会触发一轮更新, App 会进入 bailout 逻辑,且 includesSomeLane(renderLanes, workInProgress.childLanes) 为 true 所以会继续处理子节点 Son。而 Son 节点对应的更新优先级是等于此次更新的优先级的,所以 Son 不会走 bailout。
第四种情况
到这里,bailout 的三个条件就讨论完了,不过这里还有一种情况,也是我们比较常用的,那就是 shouldComponentUpdate 和 React.memo,这两个都比较熟悉,就不啰嗦了,直接贴出源代码:
shouldComponentUpdate
1 | function checkShouldComponentUpdate( |
React.memo
1 |
|
说了这么多,下面我们用一个题目来结束这篇文章。下面的代码,点击 div,Son 函数组件会重新执行吗?
1 | import * as React from 'react' |
答案是不会。下面我们来分析一下:
- 首次渲染完成后,我们有如下所示的
Fiber数结构:

- 更新时,
App进入bailout流程,但是因为子节点有更新任务,所以不会返回null,会执行cloneChildFibers(current, workInProgress);,此时,数据结构如下所示:

Parent上面有更新任务,不会走bailout逻辑,会更新Parent,执行render方法,返回新的div和span对应的ReactElement对象,而返回的props.children还是首次渲染的那个SonReactElement。所以div和span不会走bailout流程,而Son会走bailout流程:

结语
本文介绍了 beginWork 中的前半部分,即 bailout,下一篇会分析下半部分,即组件更新的相关内容,其中会涉及到面试必考的 diff 算法。