React 把组件看作状态机(有限状态机), 使用state来控制本地状态, 使用props来传递状态. 前面我们探讨了 React 如何映射状态到 UI 上(初始渲染), 那么接下来我们谈谈 React 时如何同步状态到 UI 上的, 也就是:
React 是如何更新组件的?
React 是如何对比出页面变化最小的部分?
这篇文章会为你解答这些问题.
在这之前
你已经了解了React (15-stable版本)内部的一些基本概念, 包括不同类型的组件实例、mount过程、事务、批量更新的大致过程(还没有? 不用担心, 为你准备好了从源码看组件初始渲染、接着从源码看组件初始渲染);
准备一个demo, 调试源码, 以便更好理解;
Keep calm and make a big deal !
React 是如何更新组件的?
TL;DR
依靠事务进行批量更新;
一次batch(批量)的生命周期就是从ReactDefaultBatchingStrategy事务perform之前(调用ReactUpdates.batchUpdates)到这个事务的最后一个close方法调用后结束;
事务启动后, 遇到 setState 则将 partial state 存到组件实例的_pendingStateQueue上, 然后将这个组件存到dirtyComponents 数组中, 等到 ReactDefaultBatchingStrategy事务结束时调用runBatchedUpdates批量更新所有组件;
组件的更新是递归的, 三种不同类型的组件都有自己的updateComponent方法来决定自己的组件如何更新, 其中 ReactDOMComponent 会采用diff算法对比子元素中最小的变化, 再批量处理.
这个更新过程像是一套流程, 无论你通过setState(或者replaceState)还是新的props去更新一个组件, 都会起作用.
那么具体是什么?
让我们从这套更新流程的开始部分讲起...
调用 setState 之前
首先, 开始一次batch的入口是在ReactDefaultBatchingStrategy里, 调用里面的batchedUpdates便可以开启一次batch:
// 批处理策略
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 开启一次batch
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 启动事务, 将callback放进事务里执行
return transaction.perform(callback, null, a, b, c, d, e);
}
},
};
在 React 中, 调用batchedUpdates有很多地方, 与更新流程相关的如下
// ReactMount.js
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode, // 负责初始渲染
componentInstance,
container,
shouldReuseMarkup,
context,
);
// ReactEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {
...
try {
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); // 处理事件
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
},
第一种情况, React 在首次渲染组件的时候会调用batchedUpdates, 然后开始渲染组件. 那么为什么要在这个时候启动一次batch呢? 不是因为要批量插入, 因为插入过程是递归的, 而是因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如componentWillMount或者componentDidMount)调用setState. 因此, 开启一次batch就是要存储更新(放入dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState都会生效, 用户看到的始终是最新的状态.
第二种情况, 如果你在HTML元素上或者组件上绑定了事件, 那么你有可能在事件的监听函数中调用setState, 因此, 同样为了存储更新(放入dirtyComponents), 需要启动批量更新策略. 在回调函数被调用之前, React事件系统中的dispatchEvent函数负责事件的分发, 在dispatchEvent中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState就会生效.
也就是说, 任何可能调用 setState 的地方, 在调用之前, React 都会启动批量更新策略以提前应对可能的setState
那么调用 batchedUpdates 后发生了什么?
React 调用batchedUpdates时会传进去一个函数, batchedUpdates会启动ReactDefaultBatchingStrategyTransaction事务, 这个函数就会被放在事务里执行:
// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务
var ReactDefaultBatchingStrategy = {
...
batchedUpdates: function(callback, a, b, c, d, e) {
...
return transaction.perform(callback, null, a, b, c, d, e); // 将callback放进事务里执行
...
};
ReactDefaultBatchingStrategyTransaction这个事务控制了批量策略的生命周期:
// ReactDefaultBatchingStrategy.js
var FLUSH_BATCHED_UPDATES = {
initial