setState做为react中使用最频繁的一个API,在这里简单分享它的实现机制。
没错本文是一篇讲源码的文章,但尽可能避免作代码的搬运工,根据setState的使用场景进行解析,源码基于react v16.4.3-alpha.0vue
网上有不少讲解fiber的文章大多在描述fiber的算法。实际上fiber包含数据结构和算法,按照v16以前的版本理解,fiber在源码中表示虚拟DOM的一个节点react
对react有必定了解的同窗确定知道react封装了一套本身的事件系统,<div onClick={handleClick}></div>
并非像vue同样调用addEventListener绑定事件到对应的节点上,而是经过事件委托的方式绑定到document上了
接下来咱们简单来看实现过程:git
// 获取任意一个经过react渲染获得的DOM节点
const someElement = document.getElementById('#someId')
// 打印节点元素
console.dir(someElement)
// 任何一个经过react渲染获得的DOM节点都会有`__reactEventHandlers****`这个属性
console.dir(someElement.__reactEventHandlers****)
// __reactEventHandlers中能够找到在JSX中为这个标签添加的事件属性
const onClick = someElement.__reactEventHandlers****.onClick
复制代码
有了上面的知识咱们看一下react事件系统的简易过程github
__reactEventHandlers
// 伪代码
documnet.addEventListener('click', function(event){
const target = event.target
const onClick = traget.__reactEventHandlers*****.onClick
// isBatchingUpdates全局变量后面会具体讲解到
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
// 执行事件回调
return onClick(event);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
performSyncWork()
}
})
复制代码
这里只是简要描述,实际实现要复杂不少算法
在这里能够看源码浏览器
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
this.updater 是在哪一个地方进行赋值的咱们暂时不用关心,只须要知道他被赋值为classComponentUpdater
bash
在这里能够看源码 咱们只需关心生成了update,插入到update队列,而后调用scheduleWork数据结构
// 伪代码
const classComponentUpdater = {
...
enqueueSetState(inst, payload, callback) {
const update = createUpdate(expirationTime);
// setState(payload, callback);
update.payload = payload;
update.callback = callback;
// 插入到update队列
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
...
复制代码
在这里能够看源 这一步咱们只需关心下面的这一段逻辑异步
// isWorking、isCommitting是所有变量,在后面咱们会具体分析到
if (
!isWorking ||
isCommitting ||
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
复制代码
function requestWork(root, expirationTime) {
// 将根节点添加到调度任务中
addRootToSchedule(root, expirationTime)
// isRendering是全局变量,在后面咱们会具体分析到
if (isRendering) {
return;
}
// isBatchingUpdates、isUnbatchingUpdates是全局变量
// 在第一节了解react事件时有对他们进行从新赋值
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
....
performWorkOnRoot(root, Sync, false);
}
return;
}
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
复制代码
handleClick(){
this.setState({
name: '吴彦祖'
})
console.log(this.state.name) // >> 狗蛋
this.setState({
age: '18'
})
console.log(this.state.age) // >> 40
}
复制代码
第一节中了解到在执行事件回调handleClick前isBatchingUpdates = true
,滚动到看第二节的源码过程,最终在第五步requestWork
中会执行数据结构和算法
function requestWork(){
...
if (isBatchingUpdates) {
return
}
...
}
复制代码
第一个setState也就到此为止被return
,接着执行第二个setState一样到这一步为止。
如今咱们能知道什么呢?
在交互事件中的setState每次执行只是建立了一个新的
update
,而后添加到enqueueUpdate
,setState并无直接触发react的update
再回头看第一节中react的事件过程,当handleClick
执行后会立马调用performWork
开始react的update过程
理一下整个过程,交互事件中的由于
isBatchingUpdates = true
会先收集全部的update到enqueueUpdate
中,交互事件回调执行完后再调用performWork
一次更新全部的state
从源码能够看到这整个过程对浏览器来讲都是同步的,一步一步顺序执行;对于开发者来讲,执行setState后由于要进行批处理操做,而延后了react的更新
在1中咱们知道由于isBatchingUpdates = true
的缘由执行setState后没法直接拿到新的state,若是咱们能够避免isBatchingUpdates的问题结果又会怎样
handleClick(){
setTimeout(() => {
this.setState({
name: '吴彦祖'
})
console.log(this.state.name) // >> 吴彦祖
this.setState({
age: '18'
})
console.log(this.state.age) // >> 18
})
}
复制代码
经过setTimeout执行setState
,也就没有react的事件系统什么事了, isBatchingUpdates
的默认为false,看第二节第五步,每次setState都会执行performSyncWork
触发react的update,因此每次调用setState紧接着咱们就能拿到最新的state
经过在setTimeout中执行setState咱们达到了setState是同步的效果,固然经过setInterval、Promise也能达到一样的效果。
在第二节源码中能够注意到三个全局变量:isRendering
、isWorking
、isCommitting
v16中react更新有两个阶段reconciler和commit阶段
render前生命周期属于reconciler阶段:isRendering = true
、isWorking = true
触发第二节第五步:
function requestWork(){
...
if (isRendering) {
return
}
...
}
复制代码
render前生命周期不会触发新的更新,只是将新的
update
添加到enqueueUpdate
尾部,在当前更新任务中处理
render后生命周期属于commit阶段:isRendering = true
、isWorking = true
、isCommitting = true
一样会触发第二节第五步:
function requestWork(){
addRootToSchedule(root, expirationTime)
if (isRendering) {
return
}
...
}
复制代码
render后生命周期不会当即触发新的更新,固然也不会在本次更新任务中处理,这里咱们注意有一个
addRootToSchedule(root, expirationTime)
,将新的更新做为下一个更新任务
例:
修改
name
触发componentDidUpdate()
在componentDidUpdate
修改age
过程: 修改
name
开始react的update过程完成reconciler和commit阶段,由于任务中还有一个修改age
的任务,再次开始react的update过程完成reconciler和commit阶段
注意:在componentDidUpdate使用setState可能会形成死循环
react本人用的不是不少,结合官方文档暂时只能想到上述四种场景。为仅讲解setState文中刻意省略了fiber相关的过程,后面有机会会有fiber相关的分享。有什么建议欢迎在下面留言交流。