React源码系列
- React源码解析之 Fiber结构的创建
- React源码解析 之 Fiber的渲染(1)
- React源码解析 之Fiber的渲染(2)beginWork
- React源码解析 之 Fiber的渲染(3)终
在开始之前,先奉上本次文章的demo,点击这里,进行跳转。
前言
相信你已经对React框架各种语法了然于心,能够熟练的应用于各种业务场景,造各种轮子,甚至孰能生巧,各种性能优化,信手拈来。
如何在了解源码后,可以实现具有相同能力的setState
呢?
在使用React
类组件的时候,你是否会有疑问:
疑问一:为什么我连续调用setState
后,打印日志代码显示都是同一个值。
疑问二:为什么setState()
接收一个函数后,打印日志,显示就正常了。
疑问三:为什么调用setState
是异步执行的。
带着这些疑问,请继续往下看
请看下面代码:
onClickBtn() {
const { count1 } = this.state;//count1--> 默认值为0
this.setState({ count1: count1 + 1});
this.setState({ count1: count1 + 1});
this.setState({ count1: count1 + 1});
this.setState({ count1: count1 + 1});
}
复制代码
如下面GIF图所示,当点击按钮时,调用onClickBtn
函数时,明明调用了四次setState
,按照我们同步的理解的话,每次点击,都应该增加4
,但是为什么每次只是增加1
?
按照我们的理解,当一个对象连续执行调用同一个函数时,我们都会给他做一个防抖,把得到的值,进行合并,那是不是React
中也是这样的。
翻开源码部分,找到react/packages/react-reconciler/src/ReactUpdateQueue.old.js
,可以看见当update.tag
为UpdateState
类型时,会调用Object.assign
函数进行merge
。
//...
case UpdateState: {
const payload = update.payload;
let partialState;
if (typeof payload === 'function') {
partialState = payload.call(instance, prevState, nextProps);
} else {
partialState = payload;
}
if (partialState === null || partialState === undefined) {
return prevState;
}
return Object.assign({}, prevState, partialState);
复制代码
所以两者上还是有相似之处的,区别在于,一个使用宏任务,一个使用微任务。
从React
的官网文档上,有专门介绍如何处理这种情况:
从上述代码中也可以看出,首先判断payload
是否是一个函数,当条件为真,调用payload
,同时,传入prevState
。
通过payload
函数的返回新的state
,当通过 Object.assign
进行合并时,得到最新状态。
反之,当payload
不是一个函数时,此时React
,尚未将最新的state
赋值给组件实例,因此,每次通过this.state
获取的状态,一直都是之前的状态,所以会把payload
合并,最终只打印了最后一次调用。
我们都知道setState
函数是个异步函数,那在React
中,setState
是如何做到异步的呢?
二、React中的异步
通过翻阅源码,当点击的时候,将performSyncWorkOnRoot
存入syncQueue
队列,执行微任务。
以下是React异步调用的核心代码。判断当前浏览器是否支持queueMicrotask
(微任务),当浏览器不支持queueMicrotask
时,通过返回Promise.resolve(null).then(fn)
函数,做兼容。
export const scheduleMicrotask: any =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof Promise !== 'undefined'
? callback =>
Promise.resolve(null)
.then(callback)
.catch(handleErrorInNextTick)
: scheduleTimeout; // TODO: Determine the best fallback here.
function handleErrorInNextTick(error) {
setTimeout(() => {
throw error;
});
}
复制代码
在MDN
中对于微任务和宏任务的解释是:
- 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
- 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。
换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
所以,同理,在代码在执行的时候,先将任务放入队列中, 当调用scheduleMicrotask
函数时,清空其队列。
那我们如何去实现一个具体相同功能的函数呢?
三、如何实现
setState
需要具备两个能力:
setState
可以接收一个函数。setState
的是异步的,具备回调函数。
1、首先创建一个Component函数,用来初始化一些基本参数,定义一个setState
函数,当调用setState
函数时,调用enqueueSetstate
函数,对partialState
和callback
进行保存。
//初始化
function Component(props) {
this.props = props;
this.updatequeue = [];
}
//定义setState函数
Component.prototype.setState = function(partialState, callback) {
this.enqueueSetstate(partialState, callback);
};
复制代码
2、对于enqueueSetstate函数
我们需要它将每次调用的setState的值保存在队列中,以便在合适的时机,进行调用。
此时还需要定义一个函数scheduleMicrotask
,判断updatequeue
数组为空的时候,进行调用,同时将flushUpdateQueue
函数传入进去,等待调用。
3、flushUpdateQueue函数
可以理解成React源码中的flushSyncCallbackQueue
函数。
Component.prototype.enqueueSetstate = function(partialState, callback) {
if (this.updatequeue.length === 0) {
this.scheduleMicrotask(this.flushUpdateQueue.bind(this));
}
const state = {payload: partialState, callback}
this.updatequeue.push(state);
};
复制代码
4、对于scheduleMicrotask
上面关于源码部分,也已经解释过了,为了完成flushUpdateQueue
函数的调用,此处写法略有不同。
Component.prototype.scheduleMicrotask = function(callback) {
return typeof queueMicrotask === "function"
? queueMicrotask(callback)
: typeof callback === "function"
? Promise.resolve(null).then(callback)
: setTimeout(callback);
};
复制代码
5、重要的是flushUpdateQueue函数
该函数需要完成两件事情,第一件事,首先判断payload是否是一个函数,条件为真,调用payload,并且传入prevState
和props
,然后进行赋值。
条件为假,直接返回对象。
最后将各个条件判断得到的payload和prevState进行合并。
Object.assign(prevState, partialState);
复制代码
注意:
我们需要在每一次while
循环中,对prevState
进行重新赋值,保证prevState
为上一次循环结束时的最新状态,才可以在后续payload
为函数时,传入的prevState
为前一个状态。
6、对于队列,队列的规则是先入先出。
因此,处理updatequeue
队列时,我们需要取出updatequeue
中的第一个值,然后再进行判断。而且需要设置一个条件,该条件为判断updatequeue
队列是否为空时,停止while
循环。
最终,调用render
函数,渲染页面,执行回调函数。
//清空队列,并render
Component.prototype.flushUpdateQueue = function() {
let item;
while (item = this.updatequeue.shift()) {
let { payload, callback } = item;
let prevState = this.state;
let partialState;
if (typeof payload !== "function") {
partialState = payload;
} else {
partialState = payload.call(this, prevState, this.props);
}
// 合并
Object.assign(prevState, partialState);
const newState = prevState;
this.state = newState;
this.render();
if(typeof callback === 'function') {
callback.call(this);
}
};
};
复制代码
看下成果,是否满足我们的要求。
结果基本和React中的setState保持一致。
最后,点击这里,进入demo
四、结束
整个过程,最重要的核心点在于使用微任务进行回调并清空队列。
我们需要考虑的是,传入微任务的函数,会在事件循环中什么时候开始调用,是在开始之前,还是开始之后进行调用。
写此文章的目的,还是在于对事件循环(eventLoop
)的掌握,以及场景应用。如果能举一反三,那么对我们以后的编程思路也会大有提升。