在入门学习react的setState的时候,各种教程和论坛博客基本上都是持有的这种观点:
"setState是异步的",又或者 “setState某种情况下是异步,某种情况系下是同步的”。
当时的我看到这些观点的时候,一度以为setState是被异步方法所包裹,就像Vue中的nextTick那样,是在事件循环中的异步队列里执行的。
无可厚非的是这些观点并不是错的,只是每个人的理解不同。我对“异步”的理解就是诸如setTimeout,promise等,即在事件循环中在异步队列中执行的操作。如果你和我是持有这样相同的观点,那么setState对我们来说它便是同步更新的!!!,是的,你完全可以这么回答面试官,下面我们自圆其说和证明一下吧~
在我简单的阅读完react的setState更新流程后,我并没有看到setState被任何异步操作所包裹。而他的解决频繁调用setState的思路是四个字:合并更新。
没有什么比代码更容易直观的阅读和便于理解了,下面来实现一个最简单的setState吧,麻雀虽小五脏俱全,学习到他的思想才是最重要的。(曾经我以为背的API越多越厉害,惭愧~)
let state = {num:10}
function setState(newState){
Object.assign(state,newState)
}
function changeState(){
let newState = {num:20}
setState(newState)
console.log(state)
}
为什么setState不这样设计呢?他不香吗?
setState和vue中的响应式都会解决一个问题:多次更新。当setState被调用后,或者vue的响应式数据被多次改变后(假设100次)也就是框架们会为你带来100次的render,显然是个性能灾难,vue是利用了事件循环,并且过滤掉相同的改变方法,最后异步清空。上文已经提过,setState是利用合并更新(批量更新)的。那么怎么实现这个设计思路呢?把传入的状态储存起来怎么样?试试吧~
//下面只是一个思路,请不要复制它并执行!!!
let state = {num:10}
//存储着每次setState进来的新状态
let stateUpdateQueue = []
function setState(newState){
stateUpdateQueue.push(newState)
}
function changeState(){
let newState = {num:20}
setState(newState)
console.log(state)
}
//一次性合并更新所有的状态的方法
function mergeUpdate(handleStateFunc){
Object.assign(state,...stateUpdateQueue)
stateUpdateQueue = []
}
我们把每次setState的状态存进队列,最后一次性清空,那么如你所见,mergeUpdate这个函数并没有执行,react在哪个阶段去执行这个清空操作呢?
***首先有个特定的环境,那便是合成事件!***须知在原生的dom事件中,react并不会帮你合并这些存储在队列中数据,也就是表现得是“同步”更新。
那么我们来模拟一下react的合成事件吧,也很简单,做一次函数切片就好
//html
<button id="btn">changeState</button>
//js
let state = {num:10}
let stateUpdateQueue = []
function setState(newState){
stateUpdateQueue.push(newState)
}
function changeState(e){
let newState = {num:20,count:100}
setState(newState)
console.log(state)
}
function mergeUpdate(handleStateFunc){
Object.assign(state,...stateUpdateQueue)
stateUpdateQueue = []
}
//++
//模拟合成事件
function bindEvent(dom, eventType, callback){
dom.addEventListener(eventType, function(event){
callback(event) //setState先执行
mergeUpdate() //后合并更新
})
}
let btn_dom = document.getElementById('btn')
bindEvent(btn_dom,'click',changeState)
现在假设我们在changeState事件中多次触发setState也仅仅只是会多push进去一个状态,最后统一mergeUpdate()合并更新,这时候在React中再触发render,在这里我们可以想象一下有个render()执行了就好
上文已经说到了,setState只有在合成事件中表现得才是“异步”,在原生dom事件中是不会存在异步得,所以我们需要加一个控制器来判断,setState是否应该立即执行
let state = {num:10}
let stateUpdateQueue = []
let controller = true // false代表为原生dom事件
function setState(newState){
//合成事件下合并更行,原生事件下直接更新
if(controller){
stateUpdateQueue.push(newState)
}else{
Object.assign(state,newState)
}
}
function changeState(e){
let newState = {num:20,count:100}
setState(newState)
console.log(state)
}
function mergeUpdate(handleStateFunc){
Object.assign(state,...stateUpdateQueue)
stateUpdateQueue = []
controller = false //合并更新完成后重置为false
}
//++
//模拟合成事件,对原生事件做一层代理,或叫作函数切片
function bindEvent(dom, eventType, callback){
dom.addEventListener(eventType, function(event){
controller = true //合成事件下controller 设置为true
callback(event) //setState先执行
mergeUpdate() //后合并更新
})
}
let btn_dom = document.getElementById('btn')
bindEvent(btn_dom,'click',changeState)
现在在setState在原生dom事件中表现为同步更新了,在合成事件中表现为“异步”更新了,而且因为有了异步任务setTimeout的存在,使这个方法永远在执行同步任务的执行栈清空后,再继续执行异步任务。当同步任务执行栈清空后,controller始终为false,所以setTimeout里得setState永远是同步执行。
到目前位置简单却能帮助理解,还能学到很多思想得setState就已经实现了~
让我们总结下:
观点:setState是同步(基于事件循环而言)更新,只是有着“异步”(基于表现形式而言)的表现。
*论证:
setState源码里并未用异步方法对setState包裹执行,即setState是在同步任务的执行栈中执行的。*
*表现为“异步”的原因:
在React合成事件的管控下(即controller === true),
setState将状态推入队列中,在调用setState的方法执行完毕后(此时仍然是拿到未更新的值)
接着合并更新(在这里发生了更新)。*
*表现为同步的原因:
脱离了React合成事件的管控下(即controller === false),异步任务和原生事件可以帮忙脱离。*
最后这里有完整的demo供你体验参考~
end~