setState
同步异步问题,React
批量更新一直是一个比较模糊的问题,本文但愿从框架设计的角度说明一下这个问题。javascript
React
有个UI = f(data)
公式:UI是由data
推导出来的,因此在写应用的时候,咱们只须要关心数据的改变,只需data ---> data'
, 那么UI ---> UI'
,在这个过程当中,咱们其实并不关心UI是怎么变化到UI‘的(即DOM
的变化),这部分工做是React
替咱们处理了。html
那么React
是如何知道当数据变化的时候,须要修改哪些DOM
的呢?最简单暴力的是,每次都从新构建整个DOM树。实际上,React使用的是一种叫virtual-dom
的技术:用JS对象来表示DOM结构,经过比较先后JS对象的差别,来得到DOM树的增量修改。virtual-dom
经过暴力的js计算,大大减小了DOM操做,让UI = f(data)
这种模型性能不是那么的慢,固然你用原生JS/jquery
直接操做DOM永远是最快的。java
除了virtual-dom
的优化,减小数据更新的频率是另一种手段,也就是React
的批量更新。 好比:react
g() {
this.setState({
age: 18
})
this.setState({
color: 'black‘
})
}
f() {
this.setState({
name: 'yank'
})
this.g()
}
复制代码
会被React
合成为一次setState
调用jquery
f() {
this.setState({
name: 'yank',
age: 18,
color: 'black'
})
}
复制代码
咱们经过伪码大概看一下setState
是如何合并的。git
setState
实现github
setState(newState) {
if (this.canMerge) {
this.updateQueue.push(newState)
return
}
// 下面是真正的更新: dom-diff, lifeCycle...
...
}
复制代码
而后f方法调用小程序
g() {
this.setState({
age: 18
})
this.setState({
color: 'black‘
})
}
f() {
this.canMerge = true
this.setState({
name: 'yank'
})
this.g()
this.canMerge = false
// 经过this.updateQueue合并出finalState
const finalState = ...
// 此时canMerge 已经为false 故而走入实际更新逻辑
this.setState(finaleState)
}
复制代码
能够看出 setState
首先会判断是否能够合并,若是能够合并,就直接返回了。浏览器
不过有同窗会问:在使用React
的时候,我并无设置this.canMerge
呀?咱们的确没有,是React
隐式的帮咱们设置了!事件处理函数,声明周期,这些函数的执行是发生在React
内部的,React
对它们有彻底的控制权。网络
class A extends React.Component {
componentDidMount() {
console.log('...')
}
render() {
return (<div onClick={() => {
console.log('hi')
}}></div>
}
}
复制代码
在执行componentDidMount
先后,React会执行canMerge
逻辑,事件处理函数也是同样,React委托代理了全部的事件,在执行你的处理函数函数以前,会执行React
逻辑,这样React
也是有时机执行canMerge
逻辑的。
批量更新是极好滴!咱们固然但愿任何setState
均可以被批量,关键点在于React
是否有时机执行canMerge
逻辑,也就是React
对目标函数有没有控制权。若是没有控制权,一旦setState
提早返回了,就再也没有机会应用此次更新了。
class A extends React.Component {
handleClick = () => {
this.setState({x: 1})
this.setState({x: 2})
this.setState({x: 3})
setTimeout(() => {
this.setState({x: 4})
this.setState({x: 5})
this.setState({x: 6})
}, 0)
}
render() {
return (<div onClick={this.handleClick}></div>
}
}
复制代码
handleClick
是事件回调,React
有时机执行canMerge
逻辑,因此x为1,2,3是合并的,handleClick
结束以后canMerge
被从新设置为false。注意这里有一个setTimeout(fn, 0)
。 这个fn会在handleClick
以后调用,而React对setTimeout并无控制权,React没法在setTimeout先后执行canMerge
逻辑,因此x为4,5,6是没法合并的,因此fn这里会存在3次dom-diff
。React没有控制权的状况有不少: Promise.then(fn)
, fetch
回调,xhr
网络回调等等。
那x为4,5,6有办法合并吗?是能够的,须要用unstable_batchedUpdates这个API,以下:
class A extends React.Component {
handleClick = () => {
this.setState({x: 1})
this.setState({x: 2})
this.setState({x: 3})
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
this.setState({x: 4})
this.setState({x: 5})
this.setState({x: 6})
})
}, 0)
}
render() {
return (<div onClick={this.handleClick}></div>
}
}
复制代码
这个API,不用解释太多,咱们看一下它的伪码就很清楚了
function unstable_batchedUpdates(fn) {
this.canMerge = true
fn()
this.canMerge = false
const finalState = ... //经过this.updateQueue合并出finalState
this.setState(finaleState)
}
复制代码
so, unstable_batchedUpdates 里面的setState也是会合并的。
forceUpdate从函数名上理解:“强制更新”。 既然是“强制更新”有两个问题容易引发误解:
class A extends React.Component{
handleClick = () => {
this.forceUpdate()
this.forceUpdate()
this.forceUpdate()
this.forceUpdate()
}
shouldComponentUpdate() {
return false
}
render() {
return (
<div onClick={this.handleClick}> <Son/> // 一个组件 </div>
)
}
}
复制代码
对于第一个问题:forceUpdate在批量与否的表现上,和setState是同样的。在React有控制权的函数里,是批量的。
对于第二个问题:forceUpdate只会强制自己组件的更新,即不调用“shouldComponentUpdate”直接更新,对于子孙后代组件仍是要调用本身的“shouldComponentUpdate”来决定的。
因此forceUpdate 能够简单的理解为 this.setState({})
,只不过这个setState
是不调用本身的“shouldComponentUpdate”声明周期的。
显示的让开发者调用unstable_batchedUpdates
是不优雅的,开发者不该该被框架的实现细节影响。可是正如前文所说,React
没有控制权的函数,unstable_batchedUpdates
好像是不可避免的。 不过 React16.x
的fiber
架构,可能有所改变。咱们看下fiber
下的更新
setState(newState){
this.updateQueue.push(newState)
requestIdleCallback(performWork)
}
复制代码
requestIdleCallback
会在浏览器空闲时期调用函数,是一个低优先级的函数。
如今咱们再考虑一下:
handleClick = () => {
this.setState({x: 1})
this.setState({x: 2})
this.setState({x: 3})
setTimeout(() => {
this.setState({x: 4})
this.setState({x: 5})
this.setState({x: 6})
}, 0)
}
复制代码
当x为1,2,3,4,5,6时 都会进入更新队列,而当浏览器空闲的时候requestIdleCallback
会负责来执行统一的更新。
因为fiber
的调度比较复杂,这里只是简单的说明,具体能不能合并,跟优先级还有其余都有关系。不过fiber
的架构的确能够更加优雅的实现批量更新,并且不须要开发者显示的调用unstable_batchedUpdates
最后,广告一下咱们开源的RN转小程序引擎alita,alita区别于现有的社区编译时方案,采用的是运行时处理JSX的方式,详见这篇文章。
因此alita
内置了一个mini-react
,这个mini-react
一样提供了合成setState/forceUpdate
更新的功能,并对外提供了unstable_batchedUpdates
接口。若是你读react源码无从下手,能够看一下alita minil-react
的实现,这是一个适配小程序的react实现, 且小,代码在github.com/areslabs/al…。
alita地址:github.com/areslabs/al…。 欢迎star & pr & issue