setState作为react中使用最频繁的一个API,在这里简单分享它的实现机制。
没错本文是一篇讲源码的文章,但尽量避免做代码的搬运工,根据setState的使用场景进行解析,源码基于react v16.4.3-alpha.0一,预备知识
1,fiber
网上有很多讲解fiber的文章大多在描述fiber的算法。实际上fiber包含数据结构和算法,按照v16之前的版本理解,fiber在源码中表示虚拟DOM的一个节点
2,react的事件系统
对react有一定了解的同学肯定知道react封装了一套自己的事件系统,<div onClick={handleClick}></div>
并不是像vue一样调用addEventListener绑定事件到对应的节点上,而是通过事件委托的方式绑定到document上了
任何通过react渲染得到的DOM树中随意选取一个节点都会有
__reactEventHandlers
这个属性
在这个对象中我们可以找到在JSX中为这个标签添加的事件属性,也就是可以拿到onClick
有了上面的知识我们看一下react事件系统的简易过程
- 点击一个按钮触发document上的click事件
- 获得事件对象event
- 通过event.target可以知道是点击的那个按钮
- 拿到按钮上面的
__reactEventHandlers
- 然后就有了onClick
// 伪代码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() }})复制代码
这里只是简要描述,实际实现要复杂很多
二,走一遍源码
1,setState实现
Component.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState');};复制代码
2,this.updater
this.updater 是在哪个地方进行赋值的我们暂时不用关心,只需要知道他被赋值为classComponentUpdater
3,classComponentUpdater
我们只需关心生成了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); }, ...复制代码
4,scheduleWork
这一步我们只需关心下面的这一段逻辑
// isWorking、isCommitting是全部变量,在后面我们会具体分析到if ( !isWorking || isCommitting || nextRoot !== root ) { const rootExpirationTime = root.expirationTime; requestWork(root, rootExpirationTime); }复制代码
5,requestWork
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); }}复制代码
好了,要了解setState的过程,追踪到这五步就可以了,下面会结合具体场景来对这整个过程具体分析
三,使用场景
1,交互事件
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是异步的?
从源码可以看到这整个过程对浏览器来说都是同步的,一步一步顺序执行;对于开发者来说,执行setState后因为要进行批处理操作,而延后了react的更新
2,setTimeout、setInterval、Promise中
在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也能达到同样的效果。
3,componentWillUpdate (render前生命周期)
先补充一个知识点
在第二节源码中可以注意到三个全局变量:isRendering
、isWorking
、isCommitting
v16中react更新有两个阶段reconciler和commit阶段
- isRendering:开始react更新就为true
- isWorking:进入reconciler阶段就为true、进入commit阶段就为true
- isCommitting:进入commit阶段就为true
render前生命周期属于reconciler阶段:isRendering = true
、isWorking = true
触发第二节第五步:
function requestWork(){ ... if (isRendering) { return } ...}复制代码
render前生命周期不会触发新的更新,只是将新的
update
添加到enqueueUpdate
尾部,在当前更新任务中处理
4,componentDidUpdate (render后生命周期)
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相关的分享。有什么建议欢迎在下面留言交流。