引言
在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 === newProps
hasLegacyContextChanged()
为false
includesSomeLane(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
还是首次渲染的那个Son
ReactElement
。所以div
和span
不会走bailout
流程,而Son
会走bailout
流程:
结语
本文介绍了 beginWork
中的前半部分,即 bailout
,下一篇会分析下半部分,即组件更新的相关内容,其中会涉及到面试必考的 diff
算法。