setState
到底是同步还是异步?很多人可能都有这种经历,面试的时候面试官给了你一段代码,让你说出输出的内容,比如这样:
constructor(props) {
super(props);
this.state = {
data: 'data'
}
}
componentDidMount() {
this.setState({
data: 'did mount state'
})
console.log("did mount state ", this.state.data);
// did mount state data
setTimeout(() => {
this.setState({
data: 'setTimeout'
})
console.log("setTimeout ", this.state.data);
})
}
而这段代码的输出结果,第一个 console.log
会输出 data
,而第二个 console.log
会输出 setTimeout
。也就是第一次 setState
的时候,它是异步的,第二次 setState
的时候,它又变成了同步的。是不是有点晕?不慌,我们去源码中看看它到底干了什么。
结论
先把结论放到前面,懒得看的同学可以直接看结论了。
只要你进入了 react
的调度流程,那就是异步的。只要你没有进入 react
的调度流程,那就是同步的。什么东西不会进入 react
的调度流程? setTimeout
setInterval
,直接在 DOM
上绑定原生事件等。这些都不会走 React
的调度流程,你在这种情况下调用 setState
,那这次 setState
就是同步的。
否则就是异步的。
而 setState
同步执行的情况下, DOM
也会被同步更新,也就意味着如果你多次 setState
,会导致多次更新,这是毫无意义并且浪费性能的。
scheduleUpdateOnFiber
setState
被调用后最终会走到 scheduleUpdateOnFiber
这个函数里面来,我们来看看这里面又做了什么:
function scheduleUpdateOnFiber(fiber, expirationTime) {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
checkForInterruption(fiber, expirationTime);
recordScheduleUpdate(); // TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
var priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
if ( // Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
// 重点!!!!!!
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
if ((executionContext & DiscreteEventContext) !== NoContext && ( // Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
我们着重看这段代码:
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
executionContext
代表了目前 react
所处的阶段,而 NoContext
你可以理解为是 react
已经没活干了的状态。而 flushSyncCallbackQueue
里面就会去同步调用我们的 this.setState
,也就是说会同步更新我们的 state
。所以,我们知道了,当 executionContext
为 NoContext
的时候,我们的 setState
就是同步的。那什么地方会改变 executionContext
的值呢?
我们随便找几个地方看看
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext;
...省略
}
function batchedUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= BatchedContext;
...省略
}
当 react
进入它自己的调度步骤时,会给这个 executionContext
赋予不同的值,表示不同的操作以及当前所处的状态,而 executionContext
的初始值就是 NoContext
,所以只要你不进入 react
的调度流程,这个值就是 NoContext
,那你的 setState
就是同步的。
useState的setState
自从 raect
出了 hooks
之后,函数组件也能有自己的状态,那么如果我们调用它的 setState
也是和 this.setState
一样的效果吗?
对,因为 useState
的 set
函数最终也会走到 scheduleUpdateOnFiber
,所以在这一点上和 this.setState
是没有区别的。
但是值得注意的是,我们调用 this.setState
的时候,它会自动帮我们做一个 state
的合并,而 hook
则不会,所以我们在使用的时候要着重注意这一点。
举个?
state = {
data: 'data',
data1: 'data1'
};
this.setState({ data: 'new data' });
console.log(state);
//{ data: 'new data',data1: 'data1' }
const [state, setState] = useState({ data: 'data', data1: 'data1' });
setState({ data: 'new data' });
console.log(state);
//{ data: 'new data' }
但是如果你自己去尝试在 function
组件的 setTimeout
中去调用 setState
之后,打印 state
,你会发现他并没有改变,这时你就会很疑惑,为什么呢?这不是同步执行的吗?
这是因为一个闭包问题,你拿到的还是上一个 state
,那打印出来的值自然是上一次的,此时真正的 state
已经被改变了。那有没有其他的方法可以观察到 function
函数的同步行为?有,我们下面再介绍。
案例分析
setTimeout
、原生事件内调用 setState
的操作确实比较少见,但是下面这种写法一定很常见了。
fetch = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('fetch data');
}, 300);
})
}
componentDidMount() {
(async () => {
const data = await this.fetch();
this.setState({data});
console.log("data: ", this.state);
// data: fetch data
})()
}
我们在 didMount
的时候发了一个请求,然后再将结果 setState
,这时候我们用了 async/await
来进行处理。
这时候我们会发现其实 setState
也会变成同步了,为什么呢?因为componentDidMount
执行完毕后,就已经退出了 react
的调度,而我们请求的代码还没有执行完毕,等结果请求回来以后 setState
才会执行。async
函数中 await
后面的代码其实是异步执行的。这和我们在 setTimeout
中执行 setState
其实是一个效果,所以我们的 setState
就变成同步的了。
如果它变成同步会有什么坏处呢?我们来看看如果我们多次调用了 setState
会发生什么。
this.state = {
data: 'init data',
}
componentDidMount() {
setTimeout(() => {
this.setState({data: 'data 1'});
// console.log("dom value", document.querySelector('#state').innerHTML);
this.setState({data: 'data 2'});
// console.log("dom value", document.querySelector('#state').innerHTML);
this.setState({data: 'data 3'});
// console.log("dom value", document.querySelector('#state').innerHTML);
}, 1000)
}
render() {
return (
<div id="state">
{this.state.data}
</div>
);
}
这是在浏览器运行的结果
这样来看的话,其实也并没有什么,每次刷新后最终还是会显示 data 3
,但是我们将代码中 console.log
的注释去掉,再看看:
我们每次都能在 DOM
上拿到最新的 state
,这是因为 react
已经把 state
的修改同步更新了,但是为什么界面没有显示出来?因为对浏览器来说,渲染线程 和 js线程 是互斥的, react
代码运行时浏览器是没办法渲染的。所以实际上我们已经把 DOM
更新了,但是 state
又被修改了, react
只好再做一次更新,这样反复了三次,最后 react
代码执行完毕后,浏览器才把最终的结果渲染到了界面上。这也就意味着其实我们已经做了两次无用的更新。
我们把 setTimeout
去掉,就会发现三次都输出了 init data
,因为此时的 setState
是异步的,会把三次更新合并到一次去执行。
所以当 setState
变成同步的时候就要注意,不要写出让 react
多次更新组件的代码,这是毫无意义的。
而这里也回答了之前提出的问题,如果我们想在 function
函数中观察到同步流程,大家可以去试试当你在 setTimeout
中 setState
之后, DOM
里面的内容会不会改变。
结语
react
已经帮助我们做了很多优化措施,但是有时候也会因为代码不同的实现方式而导致 react
的性能优化失效,相当于我们自己做了反优化。所以理解 react
的运行原理对我们日常开发确实是很有帮助的。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!