实现一个最简单的React.setState,学习思想和原理

399 阅读5分钟

在入门学习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~