React 性能優(yōu)化之 setState

React 組件狀態(tài)

React 把組件看成是一個狀態(tài)機(State Machines)脯宿。通過與用戶的交互亩进,實現(xiàn)不同狀態(tài),然后渲染 UI膨俐,讓用戶界面和數(shù)據(jù)保持一致勇皇。

setState() 則是 React 組件狀態(tài)更新的入口,調(diào)用 setState() 會對一個組件的 state 對象安排一次更新焚刺。當 state 改變了敛摘,該組件就會重新渲染。

一次組件更新的過程其實很復雜乳愉,包括 React 生命周期鉤子的執(zhí)行兄淫、虛擬DOM 的創(chuàng)建屯远、diff 對比、真實DOM 的創(chuàng)建 等等捕虽。

setState 批量/合并 更新

那么問題來了慨丐,是不是每次調(diào)用 setState() 都會觸發(fā)組件重新渲染呢?

如果不確定的話泄私,我們就來做個試驗驗證一下房揭。由于 React 組件每次渲染都會調(diào)用 componentDidUpdate 生命周期方法,我們可以在這個方法中打個日志:

export default class App extends Component {
  constructor (p) {
    super(p)
    this.state = {
      name: "peak",
      age: 10
    }
  }

  componentDidUpdate() {
    // 組件更新時觸發(fā)
    console.log("組件更新")
  }

  handleClick = () => {
    this.setState({
      name: "peak1"
    })
    this.setState({
      age: 11
    })
  }

  render () {
    return (
      <View>
        <Text>姓名:{this.state.name}晌端,年齡:{this.state.age}</Text>
        <TouchableOpacity onPress={this.handleClick}>
          <Text>更新組件</Text>
        </TouchableOpacity>
      </View>
    )
  }
}

可以發(fā)現(xiàn)點擊按鈕觸發(fā) setState() 時捅暴,componentDidUpdate 只走了一次,也就是說咧纠,并不是每次調(diào)用 setState() 都會觸發(fā)組件重新渲染蓬痒。

在這個案例中,多個 setState() 被合并成了一次更新惧盹,這就是 setState() 的批量更新乳幸,或者稱為 合并更新。

setState() 的合并更新還有另一種表達方式钧椰,就是我們常說的 異步粹断,異步的 setState() 表現(xiàn)為:調(diào)用 setState() 之后無法立刻獲取到最新的 this.state。通過下面的日志可以直觀的發(fā)現(xiàn)這一點:

handleClick = () => {
    this.setState({
      name: "peak1"
    })
    console.log("name=",this.state.name) // 打拥障肌:peak
    this.setState({
      age: 11
    })
    console.log("age=",this.state.age) // 打悠柯瘛:10
}

What?還有同步的 setState诊沪?

實際上合并更新是 React 的一種優(yōu)化策略养筒,目的在于避免頻繁的觸發(fā)組件重新渲染,但是這個優(yōu)化是有條件的端姚,并不是所有的 setState() 都能被合并晕粪。

下面是 setState 的偽代碼:

setState(newState) {
    if (this. isBatchingUpdates) {
        this.updateQueue.push(newState)
        return 
    }

    // 下面是真正的更新: 修改 this.state,dom-diff, lifeCycle...
    ...
}

setState 會通過一個變量來判斷當前狀態(tài)變更是否能夠被合并渐裸,如果可以合并巫湘,就會將本次更新緩存起來,等到后面來一次性更新昏鹃;如果不可以合并尚氛,就會立即更新組件。

意思就是洞渤,當 isBatchingUpdates 為 false 時阅嘶,setState() 會立即觸發(fā)組件渲染,同時 this.state 的值也會相應(yīng)的變化载迄,我們能夠立即拿到最新的 this.state 值讯柔。此時的 setState() 表現(xiàn)并非是 異步抡蛙,而是 同步 的。

從這里可以看出磷杏,setState(x) 并不等于 this.state = x溜畅。修改 this.state 的時機被 React 封裝了一層,只有當真正去渲染組件的時候 this.state 的值才會變化极祸。這就造成了我們看到的 同步異步 的現(xiàn)象慈格。

有的人可能會說,同步 很好啊遥金,我能夠立即獲取到最新的 this.state 值浴捆,很直觀。有這種想法的人忽略了一個重要的問題稿械,就是 在同步場景中选泻,每次調(diào)用 setState() 變更狀態(tài)都會觸發(fā)組件重新渲染,導致性能下降美莫。 正因為如此页眯,所以 React 才引入合并更新來避免組件頻繁的重新渲染。

那么問題又來了厢呵,既然 同步 更新會導致性能下降窝撵,那為什么 React 不直接全都用 異步 呢,這樣就能合并更新了襟铭。為了找到答案碌奉,我們接著往下看。

setState 什么時候是同步寒砖,什么時候是異步赐劣?

React 的更新是基于 Transaction(事務(wù))的,Transacation 就是給目標函數(shù)包裹一下哩都,加上前置和后置的 hook魁兼,在開始執(zhí)行之前先執(zhí)行 initialize hook,結(jié)束之后再執(zhí)行 close hook漠嵌,這樣搭配上 isBatchingUpdates 這樣的布爾標志位就可以實現(xiàn)目標函數(shù)調(diào)用棧內(nèi)的多次 setState() 全部入 pending 隊列璃赡,結(jié)束后統(tǒng)一 apply 了。

這里的 目標函數(shù) 指的是 React 控制的函數(shù)献雅,這樣的函數(shù)主要有兩類:React 合成事件生命周期鉤子; 而 setTimeout 這樣的異步方法是脫離事務(wù)的塌计,React 管控不到挺身,所以就沒法對其中的 setState() 進行合并了。

我們結(jié)合下面的 Demo 來具體分析一下:

class App 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 (<View>
          <TouchableOpacity onPress={this.handleClick}>
              <Text>更新組件</Text>
          </TouchableOpacity>
        </View>
    }
}

1锌仅、handleClick 是 React 合成事件的回調(diào)章钾,React 有控制權(quán)墙贱,在開始執(zhí)行該函數(shù)的時候會將 isBatchingUpdates 設(shè)置為 true,所以 x 為 1贱傀、2惨撇、3 是合并的;
2府寒、開始執(zhí)行 setTimeout魁衙,這里會將 setTimeout 的回調(diào)函數(shù)加入了事件循環(huán)的宏任務(wù)中,等待主線程完成所有任務(wù)后來進行調(diào)度株搔;
3剖淀、handleClick 結(jié)束之后 isBatchingUpdates 被重新設(shè)置為 false;
4纤房、此時主線程的函數(shù)已出棧纵隔,開始執(zhí)行 setTimeout 的回調(diào)函數(shù),由于 isBatchingUpdates 的值已經(jīng)變?yōu)榱?false炮姨,所以 x 為 4捌刮、5、6 沒有被合并更新舒岸,每一次的 setState() 都是同步執(zhí)行的绅作;
5、總共觸發(fā)了 4 次組件渲染吁津,其中有 2 次是冗余的棚蓄。

總結(jié)為如下:
  • 由 React 控制的事件處理程序、生命周期鉤子中的 setState() 是異步的碍脏;
  • React 控制之外的事件中調(diào)用 setState() 是同步的梭依。比如網(wǎng)絡(luò)請求、setTimeout典尾、setInterval役拴、Promise 等;
  • setState() 的 “異步” 并不是說內(nèi)部由異步代碼實現(xiàn)钾埂,其實本身執(zhí)行的過程和代碼都是同步的河闰,只是合成事件和鉤子函數(shù)的調(diào)用順序在更新之前,導致在合成事件和鉤子函數(shù)中沒法立馬拿到更新后的值褥紫,形成了所謂的 “異步”姜性。

由此可以看出 React 對于 setState() 的同步更新其實是迫于無奈,是 React 無法控制的髓考。React 當然想目標函數(shù)中的 setState() 都是異步更新的部念,這樣性能也是最好的,能夠避免組件頻繁的更新渲染,但是條件不允許儡炼,React 辦不到妓湘。

那我們能不能在寫代碼的時候規(guī)避同步的 setState() 調(diào)用呢?這是不可能的乌询,除非你的程序非常簡單且不需要跟后臺進行通信榜贴,只要你的程序要請求網(wǎng)絡(luò)接口,那么就會產(chǎn)生同步的 setState() 調(diào)用妹田。那難道就沒有辦法對同步的 setState() 進行優(yōu)化唬党,讓其合并更新嗎?

setState 手動合并(同步轉(zhuǎn)異步)

React 合成事件秆麸、生命周期鉤子 都在 React 的控制范圍內(nèi)初嘹,所以它能夠?qū)⑺麄?strong>自動加入 React 事務(wù)中,讓其中的 setState() 合并更新沮趣。對于 React 無法控制的目標函數(shù)屯烦,React 其實也有提供手動加入事務(wù)的 API,就是 unstable_batchedUpdates房铭。

我們將上面 setTimeout 中的代碼做一下調(diào)整:

class App extends React.Component {
    handleClick = () => {
        this.setState({x: 1})
        this.setState({x: 2})
        this.setState({x: 3})

        setTimeout(() => {
            // 手動將目標函數(shù)加入 React 事務(wù)中驻龟,讓其合并更新
            unstable_batchedUpdates(() => {
              this.setState({x: 4})
              this.setState({x: 5})
              this.setState({x: 6})
            })
        }, 0)
    }   

    render() {
        return (<View>
          <TouchableOpacity onPress={this.handleClick}>
              <Text>更新組件</Text>
          </TouchableOpacity>
        </View>
    }
}

x 為 1、2缸匪、3 在一個可控的目標函數(shù)中翁狐,是合并更新的;而 x 為 4凌蔬、5露懒、6 使用了 unstable_batchedUpdates 加入事務(wù),也是合并更新的砂心⌒复剩總共有 2 次更新,相較于之前的 4 次減少了 2 次辩诞。

unstable_batchedUpdates API 的原理如下:

function unstable_batchedUpdates(fn) {
    this.isBatchingUpdates = true
    fn()
    this.isBatchingUpdates = false
    const finalState = ...  //通過this.updateQueue合并出finalState
    this.setState(finaleState)
}

這個 API 在 React 和 React Native 中的引入方式有所不同:

  • react 中通過 react-dom 進行引入
import { unstable_batchedUpdates } from "react-dom";
  • react-native 中則直接從 react-native 庫中引入
import { unstable_batchedUpdates } from "react-native";

React 的這個 API 確實能夠?qū)⑼降?code>setState() 轉(zhuǎn)換為異步來進行合并更新坎弯,避免組件頻繁渲染。

但是根據(jù)其前綴 unstable 也可以看出來译暂,這個 API 不是穩(wěn)定的抠忘。實際上這是 React 實驗性的 API 之一,并沒有全力推給到開發(fā)者去使用外永,所以如果不是特別影響性能崎脉,可以不用強制用這個 API 去合并 setState()

setState 的隱藏 API

我們在使用 setState 時用的最多就是給它傳一個對象伯顶,像下面這樣:

this.setState({count: 1})
如果 setState 中的 count 需要依賴之前的值荧嵌,你會怎么處理:

1呛踊、第一種方法:使用 setState 的第二個參數(shù)

this.setState({ count: this.state.count + 1 }, () => {
    // 依賴當前 count 的值
    this.setState({ count: this.state.count + 1 })
})

setState() 的第二個參數(shù)接收一個函數(shù),這個函數(shù)會在當前 setState() 更新完組件之后觸發(fā)啦撮。這種寫法有兩個缺陷:

  • 破壞了 React 合并更新的優(yōu)化,會導致組件渲染兩次汪厨;
  • 同時這種寫法會導致嵌套太深赃春,很不美觀。

2劫乱、第二種方法:將 setState 轉(zhuǎn)為同步執(zhí)行

setTimeout(() => {
  this.setState({ count: this.state. count + 1 })
  this.setState({ count: this.state. count + 1 })
})

通過 setTimeout 能夠?qū)?setState() 轉(zhuǎn)為同步代碼织中,這樣就能夠立即獲取到最新的 this.state 值。這個方法不存在嵌套衷戈,但是和上面一樣狭吼,會導致組件渲染兩次。

3殖妇、終極方法:使用函數(shù)式的 setState
setState 其實有一個隱藏 API刁笙,第一個參數(shù)除了能夠接收對象之外,還能夠接收一個函數(shù)谦趣。這個函數(shù)接收先前的 state 作為參數(shù)疲吸,同時返回本次需要變更的 state,如下:

this.setState((state) => {
  return { count: state.count + 1 }
})
this.setState((state) => {
  return { count: state.count + 1 }
})

函數(shù)式的 setState() 能夠保證第一個函數(shù)參數(shù)中的 state 是合并了之前所有狀態(tài)的前鹅,這樣后面的函數(shù)就能拿到前面函數(shù)執(zhí)行的結(jié)果摘悴。但是這個過程中并不會改變 this.state 的值,意思就是會等函數(shù)執(zhí)行完后才去進行渲染更新舰绘,所以組件只會渲染一次蹂喻,沒有破壞 React 合并更新的優(yōu)化。

在同一個目標函數(shù)中不要混用函數(shù)式和對象式這兩種API
// 1
this.setState((state) => {
  return { count: state.count + 1 }
})
// 2
this.setState({ count: this.state.count + 1 })
// 3
this.setState((state) => {
  return { count: state.count + 1 }
})

1捂寿、假設(shè)一開始的 state.count 為 10
2口四、第一次執(zhí)行函數(shù)式 setState,count 為 11
3者蠕、第二次執(zhí)行對象式 setState窃祝,this.state 仍然是沒有更新的狀態(tài),所以 this.state.count 還是 10踱侣,加 1 以后又變回了 11
4粪小、最后再執(zhí)行函數(shù)式 setState,回調(diào)函數(shù)中的 state.count 的值是第二步中的到的 11抡句,這里再加 1探膊,所以最終 count 的結(jié)果是 12。

可以發(fā)現(xiàn)第二個對象式 setState 將第一個函數(shù)式設(shè)置的 count 抹掉了待榔,正確的做法是都調(diào)整為函數(shù)式的 setState逞壁,不然可能就會造成上面的問題流济。所以要避免函數(shù)式和對象式的 setState 混用,不然自己可能都會搞迷糊腌闯。

總結(jié)

在使用 React 作為開發(fā)框架的項目中绳瘟,setState() 應(yīng)該是我們接觸使用最多的 API,大家都習以為常的認為 setState() 是異步更新的姿骏,實際上有很多同步更新的場景被大家所忽略糖声,從而忽視了對于 setState 也能進行性能優(yōu)化的場景。

文章提到的 setState 性能優(yōu)化主要包含兩方面:

  • 適時地考慮使用 unstable_batchedUpdates 來手動合并更新分瘦,解決 React 無法自動合并更新的場景蘸泻。由于這個 API 不穩(wěn)定,所以未來可能會失效嘲玫,但目前在 RN 0.64.2 及之前的版本中驗證還是可以使用的悦施,暫時可以不用擔心;
  • 使用函數(shù)式的 setState() 來更新那些依賴于當前的 state 的 state去团。

本文為原創(chuàng)抡诞,轉(zhuǎn)載請注明出處

參考
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渗勘,一起剝皮案震驚了整個濱河市沐绒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌旺坠,老刑警劉巖乔遮,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異取刃,居然都是意外死亡蹋肮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門璧疗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坯辩,“玉大人,你說我怎么就攤上這事崩侠∑崮В” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵却音,是天一觀的道長改抡。 經(jīng)常有香客問我,道長系瓢,這世上最難降的妖魔是什么阿纤? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮夷陋,結(jié)果婚禮上欠拾,老公的妹妹穿的比我還像新娘胰锌。我一直安慰自己,他們只是感情好藐窄,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布资昧。 她就那樣靜靜地躺著,像睡著了一般荆忍。 火紅的嫁衣襯著肌膚如雪榛搔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天东揣,我揣著相機與錄音,去河邊找鬼腹泌。 笑死嘶卧,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的凉袱。 我是一名探鬼主播芥吟,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼专甩!你這毒婦竟也來了庐橙?” 一聲冷哼從身側(cè)響起乎赴,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后删窒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡萧求,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年腕巡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫩挤。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡害幅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岂昭,到底是詐尸還是另有隱情以现,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布约啊,位于F島的核電站邑遏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏棍苹。R本人自食惡果不足惜无宿,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枢里。 院中可真熱鬧孽鸡,春花似錦蹂午、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巷疼,卻和暖如春晚胡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嚼沿。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工估盘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人骡尽。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓遣妥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親攀细。 傳聞我的和親對象是個殘疾皇子箫踩,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容

  • React 為高性能應(yīng)用設(shè)計提供了許多優(yōu)化方案,本文列舉了其中的一些最佳實踐谭贪。 在以下場景中境钟,父組件和子組件通常會...
    Maco_wang閱讀 1,101評論 0 7
  • 什么樣的app才是一個優(yōu)秀的app呢? 安裝包的體積小 啟動速度快 使用流暢俭识、不卡頓 用戶交互友好 報錯或者閃退次...
    林銳濤閱讀 4,722評論 1 13
  • 一.JSX的優(yōu)點 1.書寫簡單 以html的方式書寫代碼 2.直接在jsx標簽上注冊事件 3.可以使用大括號語法 ...
    糖醋魚_閱讀 929評論 0 10
  • setState的同步和異步 1.為什么使用setState 開發(fā)中我們并不能直接通過修改state的值來讓界面發(fā)...
    wenzi8705_GG閱讀 383評論 0 2
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月慨削,有人笑有人哭,有人歡樂有人憂愁鱼的,有人驚喜有人失落理盆,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,536評論 28 53