react源码学习(3-1)实现
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码学习(3-1)实现相关的知识,希望对你有一定的参考价值。
状态更新
- 我们知道render阶段是用于更新的时候根据更新的内容进行fiber树的创建。开始于performSyncWorkOnRoot(同步)或者performConcurrentWorkOnRoot(异步。),而一般的流程是状态更新->render->commit
- 那从状态更新到render阶段,做了什么呢?
创建upDate对象
upDate
- 在react中,可以通过ReactDOM.render,this.setState,this.forceUpdate, useState,useReducer等等来触发状态更新。这些方法触发的场景各不同,但他们使用的是同一套状态更新机制,就是因为upDate对象,他用来保存更新状态相关的内容,在render阶段beginwork的时候会根据该对象计算新的state。
rootFiber
- 现在我们有某个fiber有这个upDate对象,那么怎么从该fiber找到rootFiber(不同的组件树拥有不同的rootFiber)去更新呢?react会调用markUpdateLaneFromFiberToRoot方法从该fiber向上遍历,找到rootFiber。
调度更新 Scheduler
- 现在我们已经找到了rootFiber了,他的某个fiber节点有upDate对象,需要更新,接着就是通知Scheduler,根据任务优先级,采用同步或者异步的方式调度本次更新。调用的方法是ensureRootIsScheduled
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
可以看到会根据调度的优先级分别调用不同的方法,启动render阶段。至此,状态更新跟render阶段已经连接上了。
整体流程就是
触发状态更新 =》 创建update对象 =〉根据fiber找到rootFiber =》调度 =》render阶段 =〉commit阶段.
update
update的分类
不同的触发更新方式所隶属的分类组件不同,比如this.setState是类组件,useState是函数组件,ReactDom.render是原生dom HostRoot。
类组件与原生dom共用一种update结构,函数组件自己有一套update结构。
先看类组件的update结构
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
- next是用来指向下一个update形成链表。我们不禁想到fiberd的fierstEffect和nextEffect形成的链表。两者有关联吗?答案是有,update被fiber节点上的updateQueue包裹着,可以同时存在多个update。这是正常的,比如多个地方调用了同一个this.setState更新同一个状态。就会存在多个update。多个update通过next形成单链表。
- fber节点最多同时存在两个updateQueue,分别对应currentFiber树的fiber节点的updateQueue和WorkInProgress fiber树的fiber节点的updateQueue。
updateQueue
updateQueue有三种类型,HostComponent,剩余两种跟update的两种类型对应。类组件和原生dom使用的updateQueue结构如:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
- baseState:更新前对应的state,Update是基于该state计算更新后的state。
- firstBaseUpdate和lastBaseUpdate:链表的头跟尾,保存着update,可能会疑惑为啥本次还没更新就有update了,因为有可能有些任务优先级低的任务在上一次render阶段的时候通过Update生成最新的state这个阶段的时候,优先级低的update没有被计算。
- shared.pending 触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。
- effects:数组。保存update.callback !== null的Update。
updateQueue的例子
假设有两个update由于优先级过低。被保留在了baseUpdate中。我们称之为n1,n2,n1.next = n2
fiber.updateQueue.firstBaseUpdate === n1;
fiber.updateQueue.lastBaseUpdate === n2;
n1.next === n2;
此时updateQueue里的update链表指向应该是
fiber.updateQueue.baseUpdate: n1 --> n2
此时我们在此fiber节点上触发两次更新,称之为n3,n4。每个update都会通过enqueueUpdate方法插入到updateQueue中,最近进来的是n3。
此时n3保存在shared.pending,而是环状链表
fiber.updateQueue.shared.pending === n3;
n3.next === n3;
用图表示就是
fiber.updateQueue.shared.pending: n3 ─────┐
^ |
└──────┘
接着轮到n4进来了,shared.pending保证每次指向的都是最后插入的update。所以如图
fiber.updateQueue.shared.pending: n4 ──> n3
^ |
└──────┘
也就是n3.next = n4, n4.next = n3。
此时已经调度完毕,准备进行render阶段。这时候shared.pending会剪断,拼接到updateQueue.lastBaseUpdate上。n3->n4的被剪断。所以updateQueue的update链表应该是
fiber.updateQueue.baseUpdate: n1 --> n2 --> n3 --> n4
在对update计算最新的state这个阶段,会遍历该链表,以fiber.updateQueue.baseState为初始state,依次遍历每个update计算新的state。优先级低的update会被忽略,当遍历更新后的state,就是此次更新计算的最新state。state的变化在render阶段产生与上次更新不同的jsx对象,通过diff算法产生出effecttag,在commit阶段被渲染。
优先级
react对状态更新赋予了优先级的概念,比如
- 生命周期 同步
- 用户输入框输入,同步
- 交互,渲染动画,优先
- 请求数据,低优先级
如何调度优先级呢
我们知道Schedule是用来调度优先级的,通过Schdeult提供的runWithPriority,该方法接受一个优先级常量和一个回调函数,回调函数一般是render阶段的入口函数,performSyncWorkOnRoot函数等。
例子
有个切换背景色的任务为n1,优先级为2(数值越低优先级越高)。用户点了切换背景色,此时产生一个update对象,lang属性(update.lang是用来保存优先级的)假设是2。然后进入render阶段,现在的updateQueue对象假设为
fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'H'
},
firstBaseUpdate: null,
lastBaseUpdate: null
shared: {
pending: u1
},
effects: null
};
然后用户又快速在输入框输入了一个HI,这时候又产生了一个update对象,为n2,优先级为n1。因为n2的优先级较高,所以中断了n1的render阶段,此时updateQueue中的shared的pending为
fiber.updateQueue.shared.pending === n2 ----> n1
^ |
|________|
// 即
n2.next === n1;
n1.next === n2;
指向最新的n2。此时n2的优先级是高于n1的,所以n2进入render阶段,而pending的环状链表会被切掉,又因为pending是指向最后一个update的所以正确的顺序应该是
n1—n2
进入render阶段后,在beginwork函数中遍历updateQueue计算新的state,因为n2的优先级高于n1,所以n1会跳过,n2先执行,重点来了
!!!!!
- 因为update之间可能有依赖关系,比如下一个update 可能依赖于上一个Update对象的state等。
!!!!!
- 所以被跳过的update及其后面所有update会成为下次更新的baseUpdate。(即n1 – n2)。
这是什么概念呢?
比如,此时n2完成了commit阶段后,updateQueue的对象为
fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'HI'
},
firstBaseUpdate: n1,
lastBaseUpdate: n2
shared: {
pending: null
},
effects: null
};
可以看到updateQueue中的update链表为n1->n2。在n2 commit阶段完成后,会继续进行调度,render剩余未完成的update对象,此次render是基于n1开始的,除了计算n1的更新,还会多计算一次n2的更新。
相应的n2的render阶段的一些生命周期函数,比如componentWillXX就会执行两次,所以才变得不安全。现在一些声明周期都会加上unsafe_标记。
当n1,n2的变化计算完毕后才会执行commit阶段进行渲染。所以n2的render阶段会运行两次。
componentWillXX为什么会不安全。
因为react16增加了优先级的概念,如果先触发优先级低的任务再触发优先级高的任务,优先级高的任务会先render,对应的一些生周就会执行。此时还没commit。当优先级高的任务render完,优先级低的任务会继续render,而因为每个update对象是有关联的,所以跳过的update以及后面所有的update会作为下一次render的链表。也就是说当低优先级的任务render之后,还会继续优先级高的render,此时优先级高的render被调用两次,导致可能触发两次生周函数,导致其变得不安全。
如何保证状态正确?
我们知道了updateQueue的大概工作流程了,那么还有问题?
- render阶段可以被中断,那么如何保证updateQueue保存的update对象 不丢失?
- 如果当前状态依赖上一个状态,如何在支持跳过优先级低的任务的同时保证状态依赖的连续性。
1 保存update对象
我们知道updateQueue对象的pending指向的环会被切断并拼接在updateQueue.lastBaseUpdate后面。
- 实际上,shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdate与current updateQueue.lastBaseUpdate后面。
- 那么当,在render阶段中断的时候,workInProgress的updateQueue会改变,所以基于currentFiber的updateQueue克隆出个updateQueue,这样就不会丢失了update。
- 当commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失。
2 保证状态的依赖性
- 在上面我们已经说过了update会有依赖性,所以跳过的update会连同后面的update一起被保存在updateQueue对象中。
- 当某个Update由于优先级低而被跳过时,保存在baseUpdate中的不仅是该Update,还包括链表中该Update之后的所有Update。
比如 A1 A2 A3 A4, A1,A3的优先级较高,跳过A2, A4,下次更新的时候update是A2 A3 A4,A3还会再render一次。
ReactDom.render
创建rootFiber和fiberRootNode并且关联他们
ReactDOM.render会创建rootFiber(组件根节点),fiberRootNode(整个应用的根节点)这一步由legacyCreateRootFromDOMContainer方法实现。
legacyCreateRootFromDOMContainer方法内部会调用createFiberRoot方法完成fiberRootNode和rootFiber的创建以及关联。并初始化updateQueue。
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRootNode
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 连接rootFiber与fiberRootNode
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 初始化updateQueue
initializeUpdateQueue(uninitializedFiber);
return root;
}
rootFiber和fiberRootNode通过current和stateNode指针相关联。
rootFiber.stateNode = fiberRootNode
fiberRootNode.current = rootFiber
创建update
调用updateContainer方法:
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...省略与逻辑不相关代码
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
// update.payload为需要挂载在根节点的组件
update.payload = {element};
// callback为ReactDOM.render的第三个参数 —— 回调函数
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将生成的update加入updateQueue
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);
// ...省略与逻辑不相关代码
}
这样整体流程就连接起来了,
从ReactDOM.render
创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)
|
|
v
创建Update对象(`updateContainer`)
|
|
v
从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
|
v
调度更新(`ensureRootIsScheduled`)
|
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
commit阶段(`commitRoot`)
react还有其他入口函数,
legacy -- ReactDOM.render(<App />, rootNode)
blocking -- ReactDOM.createBlockingRoot(rootNode).render(<App />)
concurrent -- ReactDOM.createRoot(rootNode).render(<App />)
不同的入口函数对应的模式不同
this.setState
setState内部会调用this.updater.enqueueSetState,
该方法会创建创建update并且调度update
enqueueSetState(inst, payload, callback) {
// 通过组件实例获取对应fiber
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// 获取优先级
const lane = requestUpdateLane(fiber, suspenseConfig);
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
// 赋值回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 将update插入updateQueue
enqueueUpdate(fiber, update);
// 调度update
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
this.forceUpdate
this.updater上不仅有enqueueSetState,还有enqueueForceUpdate,调用this.forceUpdate的时候会调用他。
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
// 赋值tag为ForceUpdate
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane, eventTime);
},
};
forceupdate多了个update.tag赋值还有少了payload,其他的跟setState一样。赋值tag的用处?
类组件更新需要满足两个条件,
这里是引用
const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, );
checkHasForceUpdateAfterProcessing:内部会判断本次更新的Update是否为ForceUpdate。即如果本次更新的Update中存在tag为ForceUpdate,则返回true。(判断是否是forceUpdate)
checkShouldComponentUpdate:内部会调用shouldComponentUpdate方法。以及当该ClassComponent为PureComponent时会浅比较state与props。(性能优化手段)
所以当存在tag的时候就是调用forceUpdate强制更新,那么当前的组件不会受如PureComponent性能优化影响,一定会更新。
学习文章来自:https://react.iamkasong.com/hooks/prepare.html#%E4%BB%8Elogo%E8%81%8A%E8%B5%B7
以上是关于react源码学习(3-1)实现的主要内容,如果未能解决你的问题,请参考以下文章
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段
Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段