beginWork 核心流程
问题
1、react更新逻辑是从roo开始的,那么是如何找到发生更新的组件呢?
2、fiber进行beginWork一定会render吗?beiginWork和render的关系是怎么样的呢
3、beginwork的更新流程为什么是从组件开始的?
4、childLans的作用是什么?
state改变是组件更新的唯一条件
在当前react版本,视图的更新来源于组件内部 state 的改变。如果想要更新视图,那么只有以下操作方式
-
组件自身的state发生改变
- 函数组件:useState或者 useReducer
- 类组件:setState或者forceUpdate
- 父组件更新导致的更新
- Provider祖先组件的state发生改变,当前组件使用了useContext或者消费了context,
无论是上述哪种方式更新,本质上都是state的改变
state改变是在组件对应的 fiber上的,在react中存在多种多样的fiber类型,但是在react体系中只有 函数式组件 和类组件 才能进行改变state,从而影响调和,比如hostComponet(html原生标签)是无法进行独立的更新的,只能依赖于父组件的state进行更新,但是在调和阶段,他也会作为一个任务但愿进入到workLoop
fiber是调和过程中的最小单元,每一个需要调和的 fiber 都会进入 workLoop 中
组件是最小的更新单元,React 的更新源于数据层 state 的变化
beiginWork是fiber更新的入口
我们一起深入来研究一下组件类型的fiber,类组件/函数式组件在 render 阶段的一个重要作用就是产生新的 子fiber节点 ,也就是我们常说的 rerender。接下来才能使用深度优先遍历算法遍历fiber 从而改变视图。每一个需要调和的 fiber 都要经历一个过程叫做 beginWork ,在 beginWork 流程中将执行上述各种 fiber 的更新函数
组件类型 fiber 进入到 workLoop 中,那么一定会 rerender 吗,答案是否定的
我们一起来看看下列几种情况
interface Child1Props {
count?: number;
}
const Child: FC<Child1Props> = ({count}) => {
return (
<>
<span>我是子组件1</span>
<span>来自父组件的数量为{count}</span>
</>
);
}
interface Child2Props {}
const Child: FC<Child2Props> = () => {
const [count , setCount] = useState<number>(0);
const onClick = () => {
setCount(count++);
}
return (
<>
<span>子组件2自己内部的数量为{count}</span>
<button onClick={onClick}>子组件2按钮</button>
</>)
}
interface ParentProps {}
const Parent: FC<ParentProps>(){
const [count , setCount] = useState<number>(0);
const onClick = () => {
setCount(count++);
}
return (
<>
<p>父组件的数量为 {count} </p>
<Child1 />
<Child2 />
<button onClick={onClick} >父组件按钮</button>
</>)
}
场景一:如上 demo 中,当点击 Child2 的 子组件2按钮 的时候,Child2 会渲染,那么 Child2自然会进入到 beginWork 流程中,那么疑问来了
- 问题一:父组件 Parent 没有更新,会 rerender 吗?那么有会进入 beginWork 流程吗 ?
- 问题二:Child1会进入 beginWork流程吗 ?
- 问题三:如果 Parent 会 beiginWork,那么 React 从 Root fiber 开始调和的时候,是如何找到发生更新的组件呢?
场景二:在如上 demo 中,当点击 Parent 中的父组件按钮的时候:
- 问题一:Parent因为本身的state 改变会更新,那么 Child1 和 Child2 为什么会跟着更新。
接下来我们开始以一次更新开始,分析调和过程中 beginWork 流程,在开始之前简单的了解一下lane
- lane : 更新优先级。(在一次更新任务中,将赋予给更新的 fiber 的一个更新优先级 lane。)
- childLanes:子fiber 中更新优先级。(如果当前 fiber 的 child 中有高优先级任务,那么当前 fiber 的 childLanes 等于当前优先级)。
住这两个概念对于下面流程分析很有帮助。接下来带着上面的四个问题,开始往下分析
我们思考一下调和算法是从rootfiber开始的,也就是从根root开始的,那么我们在调和过程中如何快速找到呢,如果你作为react的设计者应该如何去设计呢,当然答案很简单,如果我们在调用更新state的时候,给当钱组件一个标记,通过fiber的return指针反向去标记父节点直到组件根节点,然后在调和阶段只要找到和当前标记相同的的最后一个的节点,那么该节点即为state发生改变的组件,对应着react源码中的markUpdateLaneFromFiberToRoot ,核心代码如下
function markUpdateLaneFromFiberToRoot(sourceFiber,lane){
/* 更新当前 fiber 上 */
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
/* 更新缓存树上的 lanes */
let alternate = sourceFiber.alternate;
if (alternate !== null) alternate.lanes = mergeLanes(alternate.lanes, lane);
/* 当前更新的 fiber */
let node = sourceFiber;
/* 找到返回父级 */
let parent = sourceFiber.return;
while(parent !== null){
/* 更新 childLanes 字段 */
parent.childLanes = mergeLanes(parent.childLanes, lane);
if (alternate !== null) { alternate.childLanes = mergeLanes(alternate.childLanes, lane); }
/* 根据return指针向上遍历更新lane */
node = parent;
parent = parent.return;
}
}
markUpdateLaneFromFiberToRoot 做的事很重要。
- 首先会更新当前 fiber 上的更新优先级由于fiber 架构采用双缓冲树,所有还要更新当前 fiber 的缓冲树 alternate 上的优先级
- 然后会递归向上把父级连上的 childLanes 都更新,更新成当前的任务优先级
- 既然要从root开始调和,又不想调和整颗树,那么如何找到发生更新的组件呢?其实和我们想的是一样的,react采用childLanes作为标记,如果组件发生了更新,通过return向上递归标记改变父fiber的 childLanes,接下来从 Root Fiber 向下调和的时候,发现 childLanes 等于当前更新优先级,那么说明它的 child 链上有新的更新任务,会向下调和直到找到最后一个fiber的优先级等于当前更新优先级的fiebr
我们简单的了解一下fiber树的数据结构具体长什么样子,大致如图所示
- return指针:指向了当前fiber父节点
- child指针:父fiber指向的第一个子fiber
- sibling: 子fiber指向的下一个子fiber
在简单的了解fiber的简单的数据结构以后我们来探讨一下,整个调和过程是怎么样的呢
组件更新前如图所示
组件发生了更新如图所示进行标记并改变childLanes
从根节点开始向下调和,直到找到优先级等于当前更新优先级的fiber
- 第一阶段是组件发生更新,那么产生一个更新优先级 updateLane ,并且当前的组件lane赋值为updateLane
- 第二阶段通过return指针向上标记更新父fiber的 childLanes为updateLane
- 第三阶段是向下调和过程,如果父fiber被调和,那么父fiber的前几个兄弟节点一定会被调和阶段,并且父级的第一级子链的fiber都会进入调和流程
beginWork 流程
我们从调和阶段其实发现了一个有趣的现象,react首先先沿着child找到第一个子fiber,直到找到最后一个不存在子fiber的节点,然后沿着当前fiber的sbling进行寻找,直到当前节点不存在sbling,然后然后到父节点继续沿着父节点的sibling进行搜索,其实这个过程在recat被分为beginWork 和competeWork
beginwork阶段主要做的是向下调和的过程,既沿着child指针向下遍历以及沿着sbling指针向右遍历
但是只有发生state改变的组件才会rerender以及当前组件的后代组件会发生render,为了更好的理解这句话,我们作了如图所示
假设有一个组件 fiber 链:root -->child--> A组件 -->child--> B组件 --child--> C组件,如图所示
以B组件为例,发生更新的情况一共有4种情况
- 更新 A 的state,只会标记A和root,然后 root和A 会被调和,在没有作任何优化的前提下,B,C组件不进入调和,一定会发生render
- 更新 B 的state, B,A和root 会被标记,然后 root和A 会被调和,但是不会 rerender;组件 B 是既会进入调和,也会 rerender;组件 C 受到父组件 B 的影响,不会进入调和,但是会进入rerender
- 更新 C的State,那么 A,B 会进入调和流程,组件 C 既会进入调和,也会 rerender
- 更新 root 的state,那么 root会被标记,并进入调和阶段,A 受到root 的影响,不会进入调和,但是会进入rerender,(这也是为什么context发生改变,会导致所有后代fiber进行rerender的原因)
- renderLanes不等于B的childLanes,不会进行B的调和
/**
* @param {*} current current 树 fiber
* @param {*} workInProgress workInProgress 树 fiber
* @param {*} renderLanes 当前的 render 优先级
* @returns
*/
function beginWork(current,workInProgress,renderLanes){
/* -------------------第一部分-------------------- */
if(current !== null){
/* 更新流程 */
/* current 树上上一次渲染后的 props */
const oldProps = current.memoizedProps;
/* workInProgress 树上这一次更新的 props */
const newProps = workInProgress.pendingProps;
if(oldProps !== newProps || hasLegacyContextChanged()){
didReceiveUpdate = true;
}else{
/* props 和 context 没有发生变化,检查是否更新来自自身或者 context 改变 */
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
if(!hasScheduledUpdateOrContext){
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
}
/* 这里省略了一些判断逻辑 */
didReceiveUpdate = false;
}
}else{
didReceiveUpdate = false
}
/* 组件会被调和,进入更新阶段 */
switch(workInProgress.tag){
/* 函数组件的情况 */
case FunctionComponent: {
return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes )
}
/* 类组件的情况 */
case ClassComponent:{
return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
}
/* 元素类型 <div>, <span> */
case HostComponent:{
return updateHostComponent(current, workInProgress, renderLanes)
}
/* 其他 fiber 情况 */
}
}
从源码中我们可以看到beiginwork主要分为2个阶段
- 第一阶段
-
didReceiveUpdate :标记当前更新是否来源于父级的更新
-
首先通过 current !== null 来判断当前 fiber 是否创建过,如果 current 为 null,则进入mounted阶段, 否则进入update阶段
-
oldProps === newProps
- 判断当前组件更新前后的props是否相等
- 通过 useMemo或者meno 等方式缓存了 React element 元素
- 更新发生在当前组件本身,但是 B 组件的 props 并没有发生变化
反之如果两者不想等,证明父级 fiber 重新 rerender 导致了 props 改变,此时 didReceiveUpdate = true ,那么第一阶段完成,进入到第二阶段
-
第二阶段更新 fiber
- 函数组件调用 updateFunctionComponent然后进行 rerender
- 类组件调用 updateClassComponent然后进行 rerender
beiginWork流程图
总结
- 组件更新和调和过程。rerender 一定会调和,但是调和并不一定 rerender。。
- 标记是从state发生的改变沿着fiber的return指针向上标记
- 调和是从根接节点向下调和的,直到找到renderLanes等于lans的节点停止调和
- 进入beginWrok阶段,判断当前组件更新的原因是什么原因引起的
- 执行渲染fiber
转载自:https://juejin.cn/post/7372863316911702057