從setState, forceUpdate, unstable_batchedUpdates看React的批量更新

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

setState 批量更新

除了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網絡回調等等。

unstable_batchedUpdates 手動合併

那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的說明

forceUpdate從函數名上理解:「強制更新」。 既然是「強制更新」有兩個問題容易引發誤解:

  1. forceUpdate 是同步的嗎?「強制」會保證調用而後直接dom-diff嗎?
  2. 「強制」更新整個組件樹嗎?包括本身,子孫後代組件嗎? 這兩個問題官方文檔都沒有明確說明。
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」聲明週期的。

Fiber 的想象

顯示的讓開發者調用unstable_batchedUpdates是不優雅的,開發者不該該被框架的實現細節影響。可是正如前文所說,React沒有控制權的函數,unstable_batchedUpdates好像是不可避免的。 不過 React16.xfiber架構,可能有所改變。咱們看下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

相關文章
相關標籤/搜索