本文转载自微信公众号「勾勾的前端世界」, 作者西岭. 转载本文请联系勾勾的前端世界公众号.
这一次, 我将带你一次性搞懂 React 中常见的 setState 原理.
setState 本身的默认行为
我们在使用 React 的时候, 经常会用到 state(一句废话), 但是真正能完全搞清楚 setState 的帅哥美女, 确实没几个. 毕竟程序员都不太可能像我一样博学 (和好看). 那么, 要搞清楚它, 应该去投胎(整容) 吗?
不, 你需要先搞清楚 setState 本身的默认行为.
其实也很简单, 我们都知道, setState 可以传递对象形式的状态, 也可以传递函数形式的状态. 而不论状态是对象形式还是函数形式, 它都会先将所有状态保存起来, 然后进行状态合并, 所有状态合并完成后再进行一次性 DOM 更新.
如果状态是对象形式, 后面的状态会直接覆盖前面的状态. 类似于 Object.assign() 的合并操作.
对于对象状态这一点, 我们有请翠花, 上代码:
运行代码, Dom 中展示的结果为 1. 很显然两次 setState 只有一次生效了.
真的吗? 其实两次都有生效, 只不过这两次 setState 在执行前, 被合并成了一个. 你不能说到底是那个生效, 你可以说两个都没生效, 因为最终执行的是被合并的那个代码.
如果状态是函数形式, 那么依次调用函数进行状态累积, 所有函数调用完成后, 得到最终状态, 最终进行一次性 DOM 更新.
翠花, 再来一段代码......
明显不一样的结果就能说明, 两次都执行了, 因为函数状态并不会合并, 而是以此运行.
好了, 翠花可以先下去休息了, 前置只是我们已经梳理完了, 那么, 对于 setState 的研究就结束了吗? 当然不是, 接下来, 让我们换个场子, 继续掰滔(battle).
setState 同步 OR 异步
在面试场景中, 只要和 React 相关, 面试官一定舔着脸问你:"宝子, setState 是同步还是异步的呀" .
面对这样的无耻刁难, 我们需要先明确, 从 API 层面上说, 它就是普通的调用执行的函数, 自然是同步 API .
因此, 这里所说的同步和异步指的是 API 调用后更新 DOM 是同步还是异步的.
来, 我们有请娜塔莎, 上代码......
果然, 洋妹子端上来的代码确实不好消化, 通过结果我们发现, 非常奇怪的一个现象:
第一次事件执行显然为异步的, 先打印了两个 0,Dom 随之改变为 1 ;
第二次同样是异步的, 但是我们发现多次执行没效果 (异步?);
而第三次又是同步执行的了;
这什么情况, 洋妹子给我们下了迷药吗? 看我葵花宝典戳破它.
先说结论, 首先, 同步和异步主要取决于它被调用的环境.
如果 setState 在 React 能够控制的范围被调用, 它就是异步的.
比如合成事件处理函数, 生命周期函数, 此时会进行批量更新, 也就是将状态合并后再进行 DOM 更新.
如果 setState 在原生 JavaScript 控制的范围被调用, 它就是同步的.
比如原生事件处理函数中, 定时器回调函数中, Ajax 回调函数中, 此时 setState 被调用后会立即更新 DOM .
为什么会这样呢?
其实, 我们看到的所谓的 "异步", 是开启了 "批量更新" 模式的.
批量更新模式可以减少真实 DOM 渲染的次数, 所以只要是 React 能够控制的范围, 出于性能因素考虑, 一定是批量更新模式. 批量更新会先合并状态, 再一次性做 DOM 更新.
那么假设没有批量更新呢?
从生命周期的角度来看, 每一次的 setState 都是一个完整的更新流程, 这里面就包含了重新渲染 (re-render) 在内的很多操作, 大体的流程如下:
shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate;
re-render 本身涉及对 DOM 的操作, 它会带来较大的性能开销. 假如说 "一次 setState 就触发一个完整的更新流程" 这个结论成立, 那么每一次 setState 的调用都会触发一次 re-render, 我们的视图很可能没刷新几次就卡死了, 渲染就会出现下面这样的流程:
因此, setState 异步 (或者说是批量更新) 的一个重要动机就是避免频繁的 re-render.
在实际的 React 运行时中, setState 异步的实现方式有点类似于浏览器里的 Event-Loop:
每来一个 setState, 就把它塞进一个队列里. 等时机成熟, 再把队列里的 state 结果做合并, 最后只针对最新的 state 值走一次更新流程.
这个过程, 叫作 "批量更新", 批量更新的过程正如下面代码中的箭头流程图所示:
只要我们的同步代码还在执行,"进队列" 这个动作就不会停止. 因此就算我们在 React 中写了一个 N 次的 setState 循环, 也只是会增加 state 任务入队的次数, 并不会带来频繁的 re-render. 当 N 次调用结束后, 仅仅是 state 的任务队列内容发生了变化, state 本身并不会立刻改变.
为了更好地让你吃下娜塔莎, 哦不对, 是娜塔莎端上来的美食, 我帮你梳理了 setState 的执行流程图:
当然, 你可能看不懂这个流程图(是有多笨啊), 没关系, 下面还会有的.
如果为非批量更新模式, 调用多少次 setState 就会渲染多少次真实 DOM, 性能较低.
但是我们在某些条件下需要对 JS 控制的区域实现批量更新 ( 异步更新 DOM ) , 那应该怎么做呢?
强制批量更新
其实很简单, 我都不好意思说 so easy , 因为这玩意简直就是 so TM 的 easy .
我们只需要将代码包裹在 unstable_batchedUpdates 方法的回调函数中就可以实现强制批量更新.
具体使用方式也很简单, 从 react-dom 中引入进来, 然后将代码放入调用函数中就可以了.
(翠花和娜塔莎结婚了, 我来给大家上代码)
截止到现在, 我们成就了一对完美的爱情, 啊, 呸~
来源: http://developer.51cto.com/art/202108/678120.htm