关于React setState的实现原理(一)
前言
首先在学习react的时候就对setSate的实现有比较浓厚的兴趣,那么对于下边的代码,可以快速回答吗?
class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let me = this;
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印
setTimeout(function(){
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印
}, 0);
setTimeout(function(){
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印
}, 0);
}
render() {
return (
<h1>{this.state.count}</h1>
)
}
}
这段代码大家可能在很多地方看见过,结果是让你匪夷所思的0,0,2,3。 大部分人相信都不知道其中的原因,首先肯定会问:
- 为什么前两次为零,而加上setTimeout就可以打印出来?
- 为什么setTimeout打印出不同的结果?
那么请你接下来向下看,我首先说一下Batch Updata(批量更新)。如下图:

什么事Batch Update
在一些MV*框架中,就是将一段时间内对model的修改批量更新到view的机制。比如那前端比较火的React、vue为例。
在React中,我们在componentDidMount生命周期连续调用SetState:
componentDidMount () {
this.setState({ foo: 1 })
this.setState({ foo: 2 })
this.setState({ foo: 3 })
}
在没有Batch Update的情况下,上面的操作会导致三次组件渲染,但是使用Batch Update机制下时间上只运行了一次渲染。componentDidMount中三次对model的操作被优化为一次view更新,
不必要的Vitual Dom计算被忽略,从而提高了框架的效率。
Batch Update的实现
我们想到的可能就是数据结构中的栈和队列,比较一下还是使用一个queue来保存update,并在合适的时机对这个queue进行flush操作。那么现在有两个问题:
- 什么时候创建这个queue
- 什么时候对这个queue进行flush
那么我们要对Reac和Vue的源码进行分析,首先React:React中的Batch Update是通过Transaction(事务)来实现的。在React源码关于Transaction的部分可以用一幅画解释:

Transaction对一个函数进行包装,让React有机会在一个函数执行前和执行后运行特定的逻辑,从而完成对整个Batch Update流程的控制。
简单的说就是在要执行的函数中用事务包裹起来,在函数执行前加入initialize阶段,函数执行,最后执行close阶段。那么Batch Update中
在事件initialize阶段,一个update queue被创建。在事件中调用setState方法时,状态不会被立即调用,而是被push进Update queue中。
函数执行结束调用事件的close阶段,Update queue会被flush,这事新的状态才会被应用到组件上并开始后续的Virtual DOM更新,biff算法来对
model更新。
对比于React,Vue实现Batch update就简单多了:直接借助JS中的Event Loop。(参考阮老师的http://www.ruanyifeng.com/blog/2013/10/event_loop.html)
Vue中的核心代码就仅仅20多行,如下:
// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L122-L148
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
当model被修改时,对应的watcher会被推入Update queue, 与此同时还会在异步队列中添加一个task用于flush当前的Update queue。
这样一来,当前的task中的其他watcher会被推进同一个Update queue中。当前task执行结束后,异步队列下一个task执行,update queue
会被 flush,并进行后续的更新操作。
为了让 flush 动作能在当前 Task 结束后尽可能早的开始,Vue 会优先尝试将任务 micro-task 队列,具体来说,在浏览器环境中 Vue 会优
先尝试使用 MutationObserver API 或 Promise,如果两者都不可用,则 fallback 到 setTimeout。
对比两个框架可以发现 React 基于 Transition 实现的 Batch Query 是一个不依赖语言特性的通用模式,因此有更稳定可控的表现,但缺点
是无法完全覆盖所有情况,例如对于如下代码:
componentDidMount () {
setTimeout(_ => {
this.setState({ foo: 1 })
this.setState({ foo: 2 })
this.setState({ foo: 3 })
}, 0)
}
由于 setTimeout 的回调函数「不受 React 控制」,其中的 setState 就无法得到优化,最终会导致 render 函数执行三次。
而 Vue 的实现则对语言特性乃至运行环境有很强的依赖,但可以更好的覆盖各种情况:只要是在同一个 task 中的修改都可以进行 Batch Update 优化。
总结一下:
React 在这里的更新和事务机制使用比较通用的处理方式。
比如默认第一次应用初始化的时候是一次事务的进行,在用户交互的时候是一次新的事务开始,会在同一次同步事务中标记 batchUpdate=true,这样的做法是不破坏使用者的代码。
然后如果是 Ajax,setTimeout 等要离开主线程进行异步操作的时候会脱离当前 UI 的事务,这时候再进入此次处理的时候 batchUpdate=false,所以才会 setState 几次就 render 几次。
Vue 的策略虽然在机制上雷同,但是从根本上来讲是一种延迟的批量更新机制。
Angular 在这里也处理得很巧妙,利用 zone.js 对 task 进行拦截,对 JS 现有场景进行 AOP,这样就成功的桥接了代码。
React 的事务是纯粹的 IO 模型的适配。
那么Batch Update介绍到这里 ,在下一篇我们将参考React源码来分析setState的实现过程。
关于React setState的实现原理(一)的更多相关文章
- 关于React setState的实现原理(二)
React中的Transaction 大家学过sql server的都知道我们可以批量处理sql语句,原理其实都是基于上一篇我们说的Datch Update机制.当所有的操作均执行成功,才会执行修改操 ...
- 关于React setState的实现原理(三)
前面提到事务即将结束时,会去调用FLUSH_BATCHED_UPDATES的flushBatchedUpdates方法执行批量更新,该方法会去遍历dirtyComponents,对每一项执行perfo ...
- 动态修改JS对象的值及React setState
一.在JS里使用(非ES6) 实现场景: 给一个空对象填充某一指定数组内的值 并随机生成数量 const fruit = ['apple', 'banana', 'orange'] let fruit ...
- React Native 入门到原理(详解)
抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...
- 深入理解React:事件机制原理
目录 序言 DOM事件流 事件捕获阶段.处于目标阶段.事件冒泡阶段 addEventListener 方法 React 事件概述 事件注册 document 上注册 回调函数存储 事件分发 小结 参考 ...
- React Hooks 内部实现原理
React Hooks 内部实现原理 源码分析 // 链表 React Hooks 原理剖析 refs https://reactjs.org/docs/hooks-intro.html https: ...
- React 性能调优原理
一.React影响性能的两个地方 二.调优原理
- 深入研究React setState的工作机制
前言 上个月发表了一篇 React源码学习--ReactClass,但是后来我发现,大家对这种大量贴代码分析源码的形式并不感冒.讲道理,我自己看着也烦,还不如自己直接去翻源码来得痛快.吸取了上一次的教 ...
- JavaScript是如何工作的:编写自己的Web开发框架 + React及其虚拟DOM原理
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...
随机推荐
- linux/Mac使用du查看目录占用的磁盘大小
[1]du命令用来查看目录或文件所占用磁盘空间的大小.常用选项组合为: du -sh [2]若要查看一个目录下每个文件和文件夹的磁盘占用空间,使用如下命令: du -ah --max-depth=1 ...
- Django—Form两种保留用户提交数据的方法
用户在网页上进行表单填写时,有可能出现某项填写错误.一般情况下,用户在未发觉错误的情况下点击提交,则此表单的内容会清空,用户不得不再重新填写,这样的用户体验是及其糟糕的. 在此,我们有2种方法将用户的 ...
- POJ1006——Biorhythms(中国剩余定理)
Biorhythms Description人生来就有三个生理周期,分别为体力.感情和智力周期,它们的周期长度为23天.28天和33天.每一个周期中有一天是高峰.在高峰这天,人会在相应的方面表现出色. ...
- PAT 1128 N Queens Puzzle[对角线判断]
1128 N Queens Puzzle(20 分) The "eight queens puzzle" is the problem of placing eight chess ...
- MySQL5.7多源复制
MySQL5.7开始支持多源复制,也就是多主一从的复制架构: 使用多源复制的考虑: 1.灾备作用:将各个库汇总在一起,就算是其他库都挂了(整个机房都无法连接了),还有最后一个救命稻草: 2.备份:直接 ...
- Web前台学习总结
前台的技术有很多种,流行的框架也是枚不胜举,在这里我们只讨论html,css,js这些基本的技术,相信大家如果掌握了这些最基本的技术,其他的技术也就会使用了. 下面是一个案例的形式来讲解上述的技术. ...
- VS2010/MFC编程入门之二十(常用控件:静态文本框)
上一节鸡啄米讲了颜色对话框之后,关于对话框的使用和各种通用对话框的介绍就到此为止了.从本节开始鸡啄米将讲解各种常用控件的用法.常用控件主要包括:静态文本框.编辑框.单选按钮.复选框.分组框.列表框.组 ...
- AtCoder Beginner Contest 115 Solution
A Christmas Eve Eve Eve Solved. #include <bits/stdc++.h> using namespace std; int main() { int ...
- VS异常--未找到与约束 ContractName Microsoft.VisualStudio.Language.Intellisense.IGlyphService RequiredTypeIdentity
早上打开项目的时候突然遇到这么个错误: ======================= 未找到与约束 ContractName Microsoft.VisualStudio.Language.Inte ...
- 学习java的一点体会
在这几天的做实验.读书.写笔记的过程中我发现一个问题,就是我的知识面太窄,就比如说,学的知识都是一块一块的,没有能力去把它串起来,虽然学的很快,也写笔记总结,但马上就忘了,我想java是一个体系,需要 ...