经过观察调用栈和其余博客的介绍亲身体验下setState过程当中fiber干了什么事情node
下图是一个典型的create-react-app建立的项目,其中Text.js是我新增的子组件,在App.js中引用到。 react
App.js:算法
import React, { Component } from 'react';
import { Text } from './Text'
class App extends Component {
constructor(props) {
super(props)
this.state = {
tab: 'Welcome to React'
}
}
updateState = () => {
this.setState(() => ({tab: 'Bye Bye'}))
}
render() {
return (
<div className="App">
<Text tab={this.state.tab} updateState={this.updateState} />
</div>
);
}
}
export default App;
复制代码
Text.js:segmentfault
import React from 'react'
export const Text = (props) => {
return (
<span onClick={() => props.updateState()}>{props.tab}</span>
)
}
复制代码
state.tab的初始值是'Welcome to React',在setState中,传入了一个箭头函数,去更新state.tab的值为'Bye Bye',浏览器
//partialState为() => ({tab: ''Bye Bye}),callback没有传入
Component.prototype.setState = function (partialState, callback) {
//this为当前组件的实例
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
接着,执行了updater上的enqueueSetState方法,每个实例都会有一个updater(更新器),updater的做用在下面介绍,在当前App组件实例中,_reactInternalFiber是当前组件的fiber,而_reactInternalInstance是在react15使用的对象。bash
updater有3个方法,只用关心enqueueSetState数据结构
var updater = {
isMounted: isMounted,
/*
* instance: 上一步传入的App组件实例,
* partialState:须要执行更新的箭头函数,
* callback:undefined
*/
enqueueSetState: function (instance, partialState, callback) {
//获取到当前实例上的fiber
var fiber = get(instance);
//计算当前fiber的到期时间(优先级)
var expirationTime = computeExpirationForFiber(fiber);
//一次更新须要的配置参数
var update = {
expirationTime: expirationTime, //优先级
partialState: partialState, //更新的state,一般是函数而不推荐对象写法
callback: callback, //更新以后执行的回调函数
isReplace: false, //
isForced: false, //是否强制更新
capturedValue: null, //捕获的值
next: null, //
};
//将update上须要更新的信息添加到fiber中
insertUpdateIntoFiber(fiber, update);
//调度器调度fiber任务
scheduleWork(fiber, expirationTime);
},
//替换更新state,不关注
enqueueReplaceState: function (instance, state, callback) {},
//执行强制更新state,不关注
enqueueForceUpdate: function (instance, callback) {}
};
复制代码
下面按步骤详细看看这个函数内部的执行流程。app
获取fiber:key === instance,fiber很重要,记录了不少有用的信息,好比当前组件实例的各类属性和状态、优先级、标识等。异步
function get(key) {
return key._reactInternalFiber;
}
复制代码
也许你会很好奇fiber长什么样,图上展现的是组件实例上注入的fiber数据结构。 ide
计算到期时间:也就是计算当前fiber任务的优先级,从代码上看须要判断的条件比较多,既能够是异步更新,也能够是同步更新。在当前测试中,进入的是同步更新的流程。而同步对应的优先级就是1,因此expirationTime = 1。
//用来计算fiber的到期时间,到期时间用来表示任务的优先级。
function computeExpirationForFiber(fiber) {
var expirationTime = void 0;
if (expirationContext !== NoWork) {
//
expirationTime = expirationContext;
} else if (isWorking) {
if (isCommitting) {
//同步模式,当即处理任务,默认是1
expirationTime = Sync;
} else {
//渲染阶段的更新应该与正在渲染的工做同时过时。
expirationTime = nextRenderExpirationTime;
}
} else {
//没有到期时间的状况下,建立一个到期时间
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// 这是一个交互式更新
var currentTime = recalculateCurrentTime();
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// 这是一个异步更新
var _currentTime = recalculateCurrentTime();
expirationTime = computeAsyncExpiration(_currentTime);
}
} else {
// 这是一个同步更新
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
//这是一个交互式的更新。跟踪最低等待交互过时时间。这容许咱们在须要时同步刷新全部交互更新。
if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
lowestPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
复制代码
将update上须要更新的信息添加到fiber中:这个函数的做用就是把咱们在上面经过计算以后获得的update更新到fiber上面,实际操做是对象的赋值,跟合并是一个意思。
function insertUpdateIntoFiber(fiber, update) {
//确保更新队列存在,不存在则建立
ensureUpdateQueues(fiber);
//上一步已经将q1和q2队列进行了处理,定义2个局部变量queue1和queue2来保存队列信息。
var queue1 = q1;
var queue2 = q2;
// 若是只有一个队列,请将更新添加到该队列并退出。
if (queue2 === null) {
insertUpdateIntoQueue(queue1, update);
return;
}
// 若是任一队列为空,咱们须要添加到两个队列中。
if (queue1.last === null || queue2.last === null) {
//将update的值更新到队列1和队列2上,而后退出该函数
insertUpdateIntoQueue(queue1, update);
insertUpdateIntoQueue(queue2, update);
return;
}
// 若是两个列表都不为空,则因为结构共享,两个列表的最后更新都是相同的。因此,咱们应该只追加到其中一个列表。
insertUpdateIntoQueue(queue1, update);
// 可是咱们仍然须要更新queue2的`last`指针。
queue2.last = update;
}
复制代码
初始化的时候,fiber中的updateQueue是null,这时候,就要建立createUpdateQueue一个更新队列。alternate本质上也是fiber,它记录的是上一次setState操做的fiber,同时alternate又是fiber的一个属性。
ensureUpdateQueues的做用是确保更新队列不为null。
var q1 = void 0;
var q2 = void 0;
function ensureUpdateQueues(fiber) {
q1 = q2 = null;
// 咱们将至少有一个和最多两个不一样的更新队列。
//alternate是fiber上的一个属性,初始化是null,执行了setState的过程当中,会将当前的FiberNode保存到alternate上,下次setState时,就能读取到,能够用来作状态回滚。
var alternateFiber = fiber.alternate;
var queue1 = fiber.updateQueue;
if (queue1 === null) {
// 没有队列,就建立队列
queue1 = fiber.updateQueue = createUpdateQueue(null);
}
var queue2 = void 0;
if (alternateFiber !== null) {
queue2 = alternateFiber.updateQueue;
if (queue2 === null) {
queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
}
} else {
queue2 = null;
}
queue2 = queue2 !== queue1 ? queue2 : null;
// 使用模块变量
q1 = queue1;
q2 = queue2;
}
复制代码
//将update的值更新到queue中。
function insertUpdateIntoQueue(queue, update) {
// 将更新附加到列表的末尾。
if (queue.last === null) {
// 队列是空的
queue.first = queue.last = update;
} else {
queue.last.next = update;
queue.last = update;
}
if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
queue.expirationTime = update.expirationTime;
}
}
复制代码
scheduleWork进行调度:上面的几个步骤记得作了什么吗?拿到组件实例上的fiber,而后经过计算获得优先级和其余须要更新的fiber属性,最后更新到fiber上,同时建立了更新队列。可是react还没开始干活是否是,更新队列有了,fibre也有了,react的大脑该对fiber进行调度了。
调度的逻辑很复杂,由于影响因素太多了,我没法一一列举,只能根据当前的调用栈识别用到的部分。
//传入2个参数,fiber和优先级,内部又嵌套了一个函数scheduleWorkImpl,这个函数才是逻辑部分。
function scheduleWork(fiber, expirationTime) {
return scheduleWorkImpl(fiber, expirationTime, false);
}
复制代码
请注意当前传入的fiber是合并了update属性以后的fiber。
scheduleWorkImpl有写让人迷惑,司徒大佬的文章也没有解释清楚这个函数,一步步来看的话,recordScheduleUpdate的做用是先判断当前有没有正在提交更新或者已经在更新中的任务,应该是等updater执行完后,要用到的一些条件预设。
而后将node = fiber,别纠结为何是直接相等,接着执行循环,当前node也就是fiber不为空,根据条件,要在循环过程当中对node清空,清空以后退出函数。那么,这个清空的过程作了什么事情呢?
先是判断node里面的到期时间是否是等于NoWork,NoWork表示的是0,它表示的是当前没有在调度中的fiber,而后判断node的到期时间是否是大于传入的到期时间,若是知足条件,就将node的到期时间更新为新传入的到期时间。
而后判断alternate不为空的状况下,alternate在没有执行过setState,一般是初始化的时候是空状态,当执行过一次setState以后,就会将旧的FiberNode赋值给alternate,下面的函数中,若是alternate不为空,而且expirationTime和上一个if的判断一致的状况下,就更新alternate中的expirationTime。
上2个条件是更新到期时间的,第3个条件是判断return是否是等于null,return的含义在彻底理解fiber一文中有说到,表示当前的fiber任务向谁提交。在本demo中,当前是第一次执行,全部它的return为null。
function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
//记录调度的状态
recordScheduleUpdate();
var node = fiber;
while (node !== null) {
// 将父路径移到根目录并更新每一个节点的到期时间。
if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
node.expirationTime = expirationTime;
}
if (node.alternate !== null) {
if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
node.alternate.expirationTime = expirationTime;
}
}
if (node['return'] === null) {
if (node.tag === HostRoot) {
var root = node.stateNode;
if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime) {
// 是一个中断。 (用于性能跟踪。)
interruptedBy = fiber;
resetStack();
}
if (
// 若是咱们处于渲染阶段,咱们不须要为此更新安排此根目录,由于咱们将在退出以前执行此操做。
!isWorking || isCommitting ||
// ......除非这是与咱们渲染的根不一样的root。
nextRoot !== root) {
// 将root添加到root调度
requestWork(root, expirationTime);
}
} else {
return;
}
}
node = node['return'];
}
}
复制代码
//顾名思义,记录调度的状态
function recordScheduleUpdate() {
if (enableUserTimingAPI) {
if (isCommitting) {
//当前是否有正在提交的调度任务,确定没有啦。
hasScheduledUpdateInCurrentCommit = true;
}
//currentPhase表示当前执行到哪一个生命周期了。
if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
//当前是否有调度到某个生命周期阶段的任务
hasScheduledUpdateInCurrentPhase = true;
}
}
}
复制代码
到这里为止,updater的函数执行完了,咱们总结一下它到底作了什么事情。一共有4点:
根据调用栈,咱们看到了setState函数的执行过程,可是此时并无在浏览器上看到更新,由于具体的调度工做仍是要依靠react的核心算法去执行,updater只是将fiber更新到队列中,和肯定了更新的优先级。
后面要经历react的事件合成,Diff算法,虚拟DOM解析,生命周期执行等过程。很是多的代码,要是一一解释,能够写一本书出来了。
之后要是有时间,能够将后半部分关于setState里的() => ({tab: 'Bye Bye'})是如何更新的说说。 一切都要从createWorkInProgress(current, pendingProps, expirationTime)开始提及。
不管是国内哪位大神的博客,只要是介绍fiber的,万变不离其宗,看国外的这篇文章:fiber详解
Bye Bye,各位。