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

setState同步異步問題,React批量更新一直是一個比較模糊的問題,本文希望從框架設計的角度說明一下這個問題。

React有個UI = f(data) 公式:UI是由data推導出來的,所以在寫應用的時候,我們只需要關心數據的改變,只需data ---> data', 那麼UI ---> UI',在這個過程中,我們其實並不關心UI是怎麼變化到UI‘的(即DOM的變化),這部分工作是React替我們處理了。

那麼React是如何知道當數據變化的時候,需要修改哪些DOM的呢?最簡單暴力的是,每次都重新構建整個DOM樹。實際上,React使用的是一種叫virtual-dom的技術:用JS對象來表示DOM結構,通過比較前後JS對象的差異,來獲得DOM樹的增量修改。virtual-dom通過暴力的js計算,大大減少了DOM操作,讓UI = f(data)這種模型性能不是那麼的慢,當然你用原生JS/jquery直接操作DOM永遠是最快的。

setState 批量更新

除了virtual-dom的優化,減少數據更新的頻率是另外一種手段,也就是React的批量更新。 比如:

g() {
   this.setState({
        age: 18
    })
    this.setState({
        color: 'black‘
    })
}

f() {
    this.setState({
        name: 'yank'
    })
    this.g()
}

會被React合成爲一次setState調用

f() {
    this.setState({
        name: 'yank',
        age: 18, 
        color: 'black'
    })
}

我們通過僞碼大概看一下setState是如何合併的。

setState實現

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實現, 且小,代碼在https://github.com/areslabs/alita/tree/master/packages/wx-react

alita地址:https://github.com/areslabs/alita。 歡迎star & pr & issue

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章