React - setState源码分析(小白可读)

原创 amandakelake 教程 框架与工具 652阅读 2018-03-09 20:15:15 举报

我在学习过程中喜欢做记录,分享的是我在前端之路上的一些积累和思考,也希望能跟大家一起交流与进步。
这是我的github博客,欢迎一起学习,欢迎star

请先看官方文档

上来先看官方文档中对setState()的定义
英文文档最佳
英文-React.Component - React
中文-React.Component - React

setState()的实践与问题

先看个最简单的问题,点击按钮后,count是加2吗?

为什么会只加1?

看官网这句话

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

重点是前两句,翻译过来就是
setState()并不总是立即更新组件,它可能会进行批处理或者推迟更新。这使得在调用setState()之后立即读取this.state成为一个潜在的隐患。

先直接抛出点击按钮加2的正确答案吧,下面两种方法都OK

setState源码世界

相信能到这里的同学都知道了setState()是个既能同步又能异步的方法了,那具体什么时候是同步的,什么时候是异步的?只有去源码里面看实现是最靠谱的方式。

注:这里说的同步和异步只是“实现上看起来像同步还是异步,比如上面答案二setTimeout里面,看起来就是同步的”,实质上setState()是异步的

不管这里看不看得懂都没关系了,马上进入源码的世界。

如何快速查看react源码

上react的github仓库,直接clone下来
GitHub - facebook/react: A declarative, efficient, and flexible JavaScript library for building user interfaces.

到目前我看为止,最新的版本是16.2.0,我选了15.6.0的代码
一是为了参考前辈们的分析成果
二来,我水平有限,如果写的实在不清晰,同学们还可以参考着其他人的分析文章一起读,而不至于完全理解不了

如何切换版本?
1、找到对应版本号

2、复制15.6.0的历史记录号

3、回滚

如图,成功回滚到15.6.0版本

setState入口 => enqueueSetState

核心原则:既然是看源码,那当然就不是一行一行的读代码,而是看核心的思想,所以接下来的代码都只会放核心代码,旁枝末节只提一下或者忽略

setState()的入口文件在src/isomorphic/modern/class/ReactBaseClasses.js

React组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法

partialState顾名思义-“部分state”,这取名,意思大概就是不影响原来的state的意思吧

当调用setState()时实际上是调用了enqueueSetState方法,我们顺藤摸瓜(我用的是vscode的全局搜索),找到了这个文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

这个文件导出了一个ReactUpdateQueue对象,“react更新队列”,代码名字起的好可以自带注释,说的就是这种大作吧,在这里注册了enqueueSetState方法

enqueueSetState => enqueueUpdate

先看enqueueSetState的定义

这里只需要关注internalInstance的两个属性
_pendingStateQueue:待更新队列
_pendingCallbacks: 更新回调队列

如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue。最后执行enqueueUpdate(internalInstance);

接下来看enqueueUpdate

它执行的是ReactUpdates的enqueueUpdate方法

这个文件刚好就在旁边,不用找了src/renderers/shared/stack/reconciler/ReactUpdates.js

找到enqueueUpdate方法

enqueueUpdate方法定义

这段话对于理解setState非常重要

判断batchingStrategy.isBatchingUpdates
batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程,开始默认值为false

上面这句话的意思是:
如果处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中

如果不处于批量更新模式,对所有队列中的更新执行batchedUpdates方法,往下看下去就知道是用事务的方式批量的进行component的更新,事务在下面。

借用《深入React技术栈》Page167中一图

核心:batchedUpdates => 调用transaction

那batchingStrategy.isBatchingUpdates又是怎么回事呢?看来它才是关键

但是,batchingStrategy 对象并不好找,它是通过 injection 方法注入的,一番寻找,发现了 batchingStrategy 就是 ReactDefaultBatchingStrategy。
src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js
具体怎么找文件,又属于另一个范畴了,我们今天只专注 setState,其他的容后再说吧

相信部分同学在这里已经有些迷糊了,没关系,再坚持一下,旁枝末节先不管,只知道我们找到了核心方法batchedUpdates,马上要胜利了,别放弃(我第一次看也是这样熬过来的,一遍不行就两遍,大不了看多几遍)

先看批量更新策略-batchingStrategy,它到底是什么

终于找到了,isBatchingUpdates属性和batchedUpdates方法

如果isBatchingUpdates为true,当前正处于更新事务状态中,则将Component存入dirtyComponent中,
否则调用batchedUpdates处理,发起一个transaction.perform()

所有的 batchUpdate 功能都是通过执行各种 transaction 实现的

这是事务的概念,先了解一下事务吧

事务

这一段就直接引用书本里面的概念吧,《深入React技术栈》Page169

简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

下面这段代码应该能帮助理解

核心分析:batchingStrategy 批量更新策略

回到batchingStrategy:批量更新策略,再看看它的代码实现

可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程

还记得上面说的很重要的那段代码吗

首先,点击事件的处理本身就是在一个大的事务中(这个记着就好),isBatchingUpdates已经是true了

调用setState()时,调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理

在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中;

在事务结束的时候才通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将批量执行关闭结束事务。

到这里我并没有顺着ReactUpdates.flushBatchedUpdates方法讲下去,这部分涉及到渲染和Virtual Dom的内容,反正你知道它是拿来执行渲染的就行了。
到这里为止,setState的核心概念已经比较清楚了,再往下的内容,暂时先知道就行了,不然展开来讲一环扣一环太杂了,我们做事情要把握核心。

到这里不知道有没有同学想起一个问题
isBatchingUpdates 标志位在 batchedUpdates 发起的时候被置为 true ,那什么时候被复位为false的呢?
还记得上面的事务的close方法吗,同一个文件
src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

相信眼尖的同学已经看到了,close的时候复位,把isBatchingUpdates设置为false。

通过原型合并,事务的close 方法,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新

到这里,我们会发现,前面所有的队列、batchUpdate等等都是为了来到事务的这一步,前面都只是批收集的工作,到这里才真正的完成了批更新的操作。

再回到题目

这两段代码

第一种情况,在执行第一个setState时,本身已经处于一个点击事件触发的这个大事务中,已经触发了一个batchedUpdates,isBatchingUpdates为true,所以两个setState都会被批量更新,这时候属于异步过程,this.state并没有立即改变,执行setState只是相当于把partialState(前面说的部分state)传入dirtyComponents,最后在事务的close阶段执行flushBatchedUpdates去重新渲染。

第二种情况,有了setTimeout,两次setState都会在点击事件触发的大事务中的批量更新batchedUpdates结束之后再执行,所以他们会触发两次批量更新batchedUpdates,也就会执行两个事务和函数flushBatchedUpdates,就相当于同步更新的过程了。

我在学习过程中喜欢做记录,分享的是我在前端之路上的一些积累和思考,也希望能跟大家一起交流与进步。
这是我的github博客,欢迎一起学习,欢迎star

评论 ( 3 )
最新评论
zzgbsh 2018-03-12 14:34:50 3F

图片ALT配置

#### 引用式 且 图片尺寸强制设置为 100x40:
图片ALT配置

#### 图片居中
图片说明
图片说明

#### 文字左环绕 且 宽度固定,高度自适应:
图片说明书屋正中的墙上挂着一幅画,画着一棵古松,树底下卧着一只梅花鹿。画前面是先生的座位,一张八仙桌,一把高背椅子,桌子上照从前的样子,放着笔墨纸砚和一把戒尺。学生的书桌是从自己家里搬来的,分列在四面,鲁迅的那一张在东北角上。当年鲁迅就在那里读书、习字。有时还画画,把纸蒙在《西游记》一类的小说上描绣像。

#### 引用式 文字右环绕 且 高度固定,宽度自适应:
图片说明鲁迅的书桌上刻着一个小小的“早”字。字横着,很像一个还没开放的花骨朵,又像一支小小的火把。这个“早”字有一段来历∶鲁迅的父亲害了病,鲁迅一面上书塾读书,一面帮着母亲料理家务,几乎天天奔走于当铺和药铺之间,把家里的东西拿到当铺去换了钱,再到药铺去给父亲买药。有一天早晨,鲁迅上学迟到了。教书认真的寿镜吾老先生严厉地对他说∶“以后要早到!”鲁迅默默地回到座位上,就在那张旧书桌上刻了个“早”字,也把一个坚定的信念深深地刻在心里。从那以后,鲁迅上学再没有迟到过,而且时时早,事事早,毫不松弛地奋斗了一生。

俊辉 2018-03-12 13:58:15 2F

写的挺清晰的,大致能看懂

folat 2018-03-09 21:59:22 1F