React 源码剖析系列 - 解密 setState

this.setState() 方法应该是每一位使用 React 的同学最先熟悉的 API。然而,你真的了解 setState 么?先看看下面这个小问题,你能否正确回答。

引子
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};

问上述代码中 4 次 console.log 打印出来的 val ?#30452;?#26159;多少?

不卖关子,先揭晓答案,4 次 log 的值?#30452;?#26159;:0、0、2、3。

若结果和你心中的答案不完全相同,那下面的内容你可能会感兴趣。

同样的 setState 调用,为何表现和结果却大相径庭呢?让我们先看看 setState 到底干了什么。

setState 干了什么

上面这个流程图是一个简化的 setState 调用栈,注意其中核心的状态判断,在源码(ReactUpdates.js)中
function enqueueUpdate(component) {
// ...
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}

若 isBatchingUpdates 为 true,则把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新。先不管这个 batchingStrategy,看到这里大家应该已经大概猜出来了,文章一开始的例子中 4 次 setState 调用表现之所以不同,这里逻辑判断起了关键作用。

那么 batchingStrategy 究竟是何方神圣呢?其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法。下面是一段简化的定义代码:
var batchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
// ...
batchingStrategy.isBatchingUpdates = true;

transaction.perform(callback, null, a, b, c, d, e);
}
};

注意 batchingStrategy 中的 batchedUpdates 方法中,有一个 transaction.perform 调用。这就引出了本文要介绍的核心概念 —— Transaction(事务)。

初识 Transaction

熟悉 MySQL 的同学看到 Transaction 是否会心一笑?然而在 React 中 Transaction 的原理和行为和 MySQL 中并不完全相同,让我们从源码开始一步步开始了解。

在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 Transaction 的作用。
/*
*
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
*
*/

简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执?#23567;?#32780;在 perform 之前,先执?#20852;?#26377; wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执?#20852;?#26377;的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠?#21360;?br />
具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象?#30452;?#26377; key 为 initialize 和 close 的方法。

下面是一个简单使用 Transaction 的例子
var Transaction = require('./Transaction');
// 我们自己定义的 Transaction
var MyTransaction = function() {
// do sth.
};
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('before method perform');
},
close: function() {
console.log('after method perform');
}
}];
};
});
var transaction = new MyTransaction();
var testMethod = function() {
console.log('test');
}
transaction.perform(testMethod);
// before method perform
// test
// after method perform

当然在?#23548;?#20195;码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

说了这么多 Transaction,它到底是怎么导致上文所述 setState 的各种不同表现的呢?

解密 setState

那么 Transaction 跟 setState 的不同表现有什么关系呢?首先我们把 4 次 setState 简单归类,前两?#38382;?#20110;一类,因为他们在同一次调用栈中执行;setTimeout 中的两次 setState 属于另一类,原因同上。让我们?#30452;?#30475;看这两类 setState 的调用栈:

componentDidMout 中 setState 的调用栈

setTimeout 中 setState 的调用栈

很明显,在 componentDidMount 中直接调用的两次 setState,其调用栈更加复杂;而 setTimeout 中调用的两次 setState,调用栈则简单很多。让我们重点看看第一类 setState 的调用栈,有没有发现什么熟悉的身影?没错,就是batchedUpdates 方法,原来早在 setState 调用前,已经处于 batchedUpdates 执行的 transaction 中!

那这次 batchedUpdate 方法,又是谁调用的呢?让我们往前再追溯一层,原来是 ReactMount.js 中的_renderNewRootComponent 方法。也就是说,整个将 React 组件渲染到 DOM 中的过程就处于一个大的 Transaction ?#23567;?br />
接下来的解释就顺理成章了,因为在 componentDidMount 中调用 setState 时,batchingStrategy 的 isBatchingUpdates 已经被设为 true,所以两次 setState 的结果并没有立即生效,而是被放进了 dirtyComponents ?#23567;?#36825;也解释了两次打印this.state.val 都是 0 的原因,新的 state 还没有被应用到组件?#23567;?br />
再反观 setTimeout 中的两次 setState,因为没有前置的 batchedUpdate 调用,所以 batchingStrategy 的 isBatchingUpdates 标志位是 false,也就导致了新的 state 马上生效,没有走到 dirtyComponents 分支。也就是,setTimeout 中第一次 setState 时,this.state.val 为 1,而 setState 完成后打印时 this.state.val 变成了 2。第二次 setState 同理。

扩展阅读

在上文介绍 Transaction 时也提到了其在 React 源码中的多处应用,想必调试过 React 源码的同学应该能经常见到它的身影,像 initialize、perform、close、closeAll、notifyAll 等方法出现在调用栈里时,都说明当前处于一个 Transaction ?#23567;?br />
既然 Transaction 这么有用,我们自己的代码中能使用 Transaction 吗?很?#19978;В?#31572;案是不能。不过针对文章一开始例子中 setTimeout 里的两次 setState 导致两次 render 的情况,React 偷偷给我们暴露了一个 batchedUpdates 方法,方便我们调用。
import ReactDom, { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
this.setState(val: this.state.val + 1);
this.setState(val: this.state.val + 1);
});

当然因为这个不是公开的 API,后续存在废弃的风险,大家在业务系统里慎用哟!

注释
    test-react 文中测?#28304;?#30721;已放在 Github 上,需要自己实验探索的同学可以 clone 下来自己?#31995;?#35843;试。为了避免引入更多的概念,上文中所说到的 batchingStrategy 均指 ReactDefaultBatchingStrategy,该 strategy 在 React 初始化时由 ReactDefaultInjection 注入到 ReactUpdates 中作为默?#31995;?strategy。在 server 渲染时,则会注入不同的 strategy,有兴趣的同学请自行探索。


原文地址?#33322;?#26085;头条
回复

使用道具 举报

你可能还?#19981;?/span>
本类?#21028;?/span>
新天地棋牌 267人下载
众神仙战 139人下载
偶像梦工厂 131人下载
RE8召唤轮转 129人下载
天天象棋 119人下载
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|天地棋牌 |网站地图

GMT+8, 2019-8-17 16:33 , Processed in 1.019539 second(s), 37 queries .

快速回复 返回顶部 返回列表
亚冠吉达阿赫利对阿尔萨德
金蟾捕鱼开挂软件 中国福利彩票2019062开奖结果 老时时360开奖视频 今晚深圳风采玩法 赛马会赛马会论坛资料大全 qq宠物捕鱼大师 四川时时视频直播 十五选五往期开奖 3d电视怎么看3d pk10三码全天计划 云南时时彩计划团队 山东十一选五全天计划 时时彩打9个号码靠谱吗 河北时时开奖视频直播 北京11选5开奖历史 北京时时官网走势图