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