React 中setState更新state何時同步何時異步?

React中constructor是唯一可以初始化state的地方葵礼,也可以把它理解成一個鉤子函數(shù)号阿,該函數(shù)最先執(zhí)行且只執(zhí)行一次。

更新狀態(tài)不要直接修改this.state章咧。雖然狀態(tài)可以改變倦西,但不會觸發(fā)組件的更新能真。

應(yīng)當(dāng)使用this.setState()赁严,該方法接收兩種參數(shù):對象或函數(shù)。

  1. 對象:即想要修改的state
  2. 函數(shù):接收兩個函數(shù)粉铐,第一個函數(shù)接受兩個參數(shù)疼约,第一個是當(dāng)前state,第二個是當(dāng)前props蝙泼,該函數(shù)返回一個對象程剥,和直接傳遞對象參數(shù)是一樣的,就是要修改的state汤踏;第二個函數(shù)參數(shù)是state改變后觸發(fā)的回調(diào)织鲸。

回到主題,setState可能是異步的溪胶。對此官方有這樣一段描述:setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState()a potential pitfall.

關(guān)鍵詞:batch搂擦、defer、may哗脖。

要探究setState為什么可能是異步的瀑踢,先了解setState執(zhí)行后會發(fā)生什么扳还?

事實上setState內(nèi)部執(zhí)行過程是很復(fù)雜的,大致過程包括更新state橱夭,創(chuàng)建新的VNode氨距,再經(jīng)過diff算法比對差異,決定渲染哪一部分以及怎么渲染棘劣,最終形成最新的UI俏让。這一過程包含組件的四個生命周期函數(shù)。

  • shouleComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

需要注意的是如果子組件的數(shù)據(jù)依賴于父組件茬暇,還會執(zhí)行一個鉤子函數(shù)componentWillReceiveProps舆驶。

假如setState是同步更新的,每更新一次而钞,這個過程都要完整執(zhí)行一次沙廉,無疑會造成性能問題。事實上這些生命周期為純函數(shù)臼节,對性能還好撬陵,但是diff比較、更新DOM總消耗時間和性能吧网缝。

此外為了批次和效能巨税,多個setState有可能在執(zhí)行過程中還會被合并,所以setState延時異步更新是很合理的粉臊。

setState何時同步何時異步草添?

由React控制的事件處理程序,以及生命周期函數(shù)調(diào)用setState不會同步更新state 扼仲。

React控制之外的事件中調(diào)用setState是同步更新的远寸。比如原生js綁定的事件,setTimeout/setInterval等屠凶。

大部分開發(fā)中用到的都是React封裝的事件驰后,比如onChange、onClick矗愧、onTouchMove等灶芝,這些事件處理程序中的setState都是異步處理的。

看以下case:

constructor() {
  this.state = {
    count: 10
  }

  this.handleClickOne = this.handleClickOne.bind(this)
  this.handleClickTwo = this.handleClickTwo.bind(this)
}

render() {
  return (
    <button onClick={this.hanldeClickOne}>clickOne</button>
    <button onClick={this.hanldeClickTwo}>clickTwo</button>
    <button id="btn">clickTwo</button>
  )
}

handleClickOne() {
  this.setState({ count: this.state.count + 1})
  console.log(this.state.count)
}

輸出:10

由此可以看出該事件處理程序中的setState是異步更新state的唉韭。

componentDidMount() {
  document.getElementById('btn').addEventListener('clcik', () => {
    this.setState({ count: this.state.count + 1})
    console.log(this.state.count)
  })
}

輸出: 11

handleClickTwo() {
  setTimeout(() => {
    this.setState({ count: this.state.count + 1})
    console.log(this.state.count)
  }, 10)  
}

輸出: 11

以上兩種方式繞過React夜涕,通過js的事件綁定程序 addEventListener 和使用setTimeout/setInterval 等 React 無法掌控的 APIs情況下,setState是同步更新state属愤。

React是怎樣控制異步和同步的呢女器?

在 React 的 setState 函數(shù)實現(xiàn)中,會根據(jù)一個變量 isBatchingUpdates 判斷是直接更新 this.state 還是放到隊列中延時更新春塌,而 isBatchingUpdates 默認(rèn)是 false晓避,表示 setState 會同步更新 this.state簇捍;但是,有一個函數(shù) batchedUpdates俏拱,該函數(shù)會把 isBatchingUpdates 修改為 true暑塑,而當(dāng) React 在調(diào)用事件處理函數(shù)之前就會先調(diào)用這個 batchedUpdates將isBatchingUpdates修改為true,這樣由 React 控制的事件處理過程 setState 不會同步更新 this.state锅必。

image

多個setState調(diào)用會合并處理


render() {
  console.log('render')
}
hanldeClick() {
  this.setState({ name: 'jack' })
  this.setState({ age: 12 })
}

在hanldeClick處理程序中調(diào)用了兩次setState事格,但是render只執(zhí)行了一次。因為React會將多個this.setState產(chǎn)生的修改放在一個隊列里進(jìn)行批延時處理搞隐。

參數(shù)為函數(shù)的setState用法

先看以下case:

handleClick() {
  this.setState({
    count: this.state.count + 1
  })
}

以上操作存在潛在的陷阱驹愚,不應(yīng)該依靠它們的值來計算下一個狀態(tài)。

handleClick() {
  this.setState({
    count: this.state.count + 1
  })
  this.setState({
    count: this.state.count + 1
  })
  this.setState({
    count: this.state.count + 1
  })
}

最終的結(jié)果只加了1

因為調(diào)用this.setState時劣纲,并沒有立即更改this.state逢捺,所以this.setState只是在反復(fù)設(shè)置同一個值而已,上面的代碼等同于這樣

handleClick() {
  const count = this.state.count

  this.setState({
    count: count + 1
  })
  this.setState({
    count: count + 1
  })
  this.setState({
    count: count + 1
  })
}

count相當(dāng)于一個快照癞季,所以不管重復(fù)多少次劫瞳,結(jié)果都是加1。

此外假如setState更新state后我希望做一些事情绷柒,而setState可能是異步的志于,那我怎么知道它什么時候執(zhí)行完成。所以setState提供了函數(shù)式用法废睦,接收兩個函數(shù)參數(shù)伺绽,第一個函數(shù)調(diào)用更新state,第二個函數(shù)是更新完之后的回調(diào)嗜湃。

第一個函數(shù)接收先前的狀態(tài)作為第一個參數(shù)奈应,將此次更新被應(yīng)用時的props做為第二個參數(shù)。

increment(state, props) {
  return {
    count: state.count + 1
  }
}

handleClick() {
  this.setState(this.increment)
  this.setState(this.increment)
  this.setState(this.increment)
}

結(jié)果: 13

對于多次調(diào)用函數(shù)式setState的情況净蚤,React會保證調(diào)用每次increment時钥组,state都已經(jīng)合并了之前的狀態(tài)修改結(jié)果输硝。

也就是說今瀑,第一次調(diào)用this.setState(increment),傳給increment的state參數(shù)的count是10点把,第二調(diào)用是11橘荠,第三次調(diào)用是12,最終handleClick執(zhí)行完成后的結(jié)果就是this.state.count變成了13郎逃。

值得注意的是:在increment函數(shù)被調(diào)用時哥童,this.state并沒有被改變,依然要等到render函數(shù)被重新執(zhí)行時(或者shouldComponentUpdate函數(shù)返回false之后)才被改變褒翰,因為render只執(zhí)行一次贮懈。

讓setState接受一個函數(shù)的API的設(shè)計是相當(dāng)棒的匀泊!不僅符合函數(shù)式編程的思想,讓開發(fā)者寫出沒有副作用的函數(shù)朵你,而且我們并不去修改組件狀態(tài)各聘,只是把要改變的狀態(tài)和結(jié)果返回給React,維護(hù)狀態(tài)的活完全交給React去做抡医。正是把流程的控制權(quán)交給了React躲因,所以React才能協(xié)調(diào)多個setState調(diào)用的關(guān)系。

在同一個事件處理程序中不要混用

case:

increment(state, props) {
  return {
    count: state.count + 1
  }
}

handleClick() {
  this.setState(this.increment)
  this.setState({ count: this.state.count + 1 })
  this.setState(this.increment)
}

結(jié)果: 12

第一次執(zhí)行setState忌傻,count為11大脉,第二次執(zhí)行,this.state仍然是沒有更新的狀態(tài)水孩,所以this.state.count又打回了原形為10镰矿,加1以后變成11,最后再執(zhí)行setState俘种,所以最終count的結(jié)果是12衡怀。(render依然只執(zhí)行一次)

setState的第二個回調(diào)參數(shù)會在更新state,重新觸發(fā)render后執(zhí)行安疗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抛杨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子荐类,更是在濱河造成了極大的恐慌怖现,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玉罐,死亡現(xiàn)場離奇詭異屈嗤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吊输,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門饶号,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人季蚂,你說我怎么就攤上這事茫船。” “怎么了扭屁?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵算谈,是天一觀的道長。 經(jīng)常有香客問我料滥,道長然眼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任葵腹,我火速辦了婚禮高每,結(jié)果婚禮上屿岂,老公的妹妹穿的比我還像新娘。我一直安慰自己鲸匿,他們只是感情好雁社,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布乃戈。 她就那樣靜靜地躺著其障,像睡著了一般。 火紅的嫁衣襯著肌膚如雪帘撰。 梳的紋絲不亂的頭發(fā)上洪囤,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天徒坡,我揣著相機(jī)與錄音,去河邊找鬼瘤缩。 笑死喇完,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的剥啤。 我是一名探鬼主播锦溪,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼府怯!你這毒婦竟也來了刻诊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤牺丙,失蹤者是張志新(化名)和其女友劉穎则涯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冲簿,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡粟判,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了峦剔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片档礁。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吝沫,靈堂內(nèi)的尸體忽然破棺而出呻澜,到底是詐尸還是另有隱情,我是刑警寧澤野舶,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布易迹,位于F島的核電站,受9級特大地震影響平道,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜供炼,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一一屋、第九天 我趴在偏房一處隱蔽的房頂上張望窘疮。 院中可真熱鬧,春花似錦冀墨、人聲如沸闸衫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔚出。三九已至,卻和暖如春虫腋,著一層夾襖步出監(jiān)牢的瞬間骄酗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工悦冀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留趋翻,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓盒蟆,卻偏偏與公主長得像踏烙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子历等,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354