React中setState同步異步場景的使用
setState同步異步場景
React通過this.state來訪問state,通過this.setState()方法來更新state,當(dāng)this.setState()方法被調(diào)用的時(shí)候,React會(huì)重新調(diào)用render方法來重新渲染UI。相比較于在使用Hooks完成組件下所需要的心智負(fù)擔(dān),setState就是在使用class完成組件下所需要的心智負(fù)擔(dān),當(dāng)然所謂的心智負(fù)擔(dān)也許叫做所必須的基礎(chǔ)知識(shí)更加合適一些。
描述
setState只在合成事件和生命周期鉤子函數(shù)中是異步的,而在原生事件中都是同步的,簡單實(shí)現(xiàn)一個(gè)React Class TS例子。
import React from "react";
import "./styles.css";
interface Props{}
interface State{
count1: number;
count2: number;
count3: number;
}
export default class App extends React.Component<Props, State>{
constructor(props: Props){
super(props);
this.state = {
count1: 0,
count2: 0,
count3: 0
}
}
incrementAsync = () => {
console.log("incrementAsync before setState", this.state.count1);
this.setState({
count1: this.state.count1 + 1
});
console.log("incrementAsync after setState", this.state.count1);
}
incrementSync = () => {
setTimeout(() => {
console.log("incrementSync before setState", this.state.count2);
this.setState({
count2: this.state.count2 + 1
});
console.log("incrementSync after setState", this.state.count2);
},0);
}
incrementAsyncFn = () => {
console.log("incrementAsyncFn before setState", this.state.count3);
this.setState({
count3: this.state.count3 + 1
},
() => {
console.log("incrementAsyncFn after.1 setState", this.state.count3);
}
);
this.setState(state => {
console.log("incrementAsyncFn after.2 setState", state.count3);
return {count3: state.count3}
});
}
render(){
return <div>
<button onClick={this.incrementAsync}>異步</button>
<button onClick={this.incrementSync}>同步</button>
<button onClick={this.incrementAsyncFn}>異步函數(shù)參數(shù)</button>
</div>
}
}以此點(diǎn)擊三個(gè)按鈕的話,可以得到以下輸出。
incrementAsync before setState 0
incrementAsync after setState 0
incrementSync before setState 0
incrementSync after setState 1
incrementAsyncFn before setState 0
incrementAsyncFn after.2 setState 1
incrementAsyncFn after.1 setState 1
首先看incrementAsync的結(jié)果,在這里我們可以看出,在合成事件調(diào)用setState之后,this.state是無法立即得到最新的值。
對(duì)于incrementSync的結(jié)果,在非合成事件的調(diào)用時(shí),this.state是可以立即得到最新的值的,例如使用addEventListener、setTimeout、setInterval等。
對(duì)于incrementAsyncFn的兩個(gè)結(jié)果,首先來說after.2的結(jié)果,對(duì)于this.state也是可以得到最新的值的,如果你需要基于當(dāng)前的state來計(jì)算出新的值,那么就可以通過傳遞一個(gè)函數(shù),而不是一個(gè)對(duì)象的方式來實(shí)現(xiàn),因?yàn)?code>setState的調(diào)用是分批的,所以通過傳遞函數(shù)可以鏈?zhǔn)降剡M(jìn)行更新,當(dāng)然前提是需要確保它們是一個(gè)建立在另一個(gè)之上的,也就是說傳遞函數(shù)的setState的值是依賴于上次一的SetState的,對(duì)于after.1的結(jié)果,setState函數(shù)的第二個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),在setState批量更新完成后可以拿到最新的值,而after.2也是屬于批量更新需要調(diào)用的函數(shù),所以after.1會(huì)在after.2后執(zhí)行。
原理
React將其實(shí)現(xiàn)為異步的動(dòng)機(jī)主要是性能的考量,setState的異步并不是說內(nèi)部由異步代碼實(shí)現(xiàn),其實(shí)本身執(zhí)行的過程和代碼都是同步的,只是合成事件和生命周期鉤子函數(shù)的調(diào)用順序在批處理更新之前,導(dǎo)致在合成事件和生命周期鉤子函數(shù)中沒法立馬拿到更新后的值,形式了所謂的異步,實(shí)際上是否進(jìn)行批處理是由其內(nèi)部的isBatchingUpdates的值來決定的。setState依賴于合成事件,合成事件指的是React并不是將click等事件直接綁定在DOM上面,而是采用事件冒泡的形式冒泡到頂層DOM上,類似于事件委托,然后React將事件封裝給正式的函數(shù)處理運(yùn)行和處理。說完了合成事件再回到setState,setState的批量更新優(yōu)化也是建立在合成事件上的,其會(huì)將所有的setState進(jìn)行批處理,如果對(duì)同一個(gè)值進(jìn)行多次 setState,setState的批量更新策略會(huì)對(duì)其進(jìn)行覆蓋,取最后一次的執(zhí)行,如果是同時(shí)setState多個(gè)不同的值,在更新時(shí)也會(huì)對(duì)其進(jìn)行合并批量更新,而在原生事件中,值會(huì)立即進(jìn)行更新。
采用批量更新,簡單來說就是為了提升性能,因?yàn)椴徊捎门扛?,在每次更新?shù)據(jù)都會(huì)對(duì)組件進(jìn)行重新渲染,舉個(gè)例子,讓我們在一個(gè)方法內(nèi)重復(fù)更新一個(gè)值。
this.setState({ msg: 1 });
this.setState({ msg: 2 });
this.setState({ msg: 3 });事實(shí)上,我們真正想要的其實(shí)只是最后一次更新而已,也就是說前三次更新都是可以省略的,我們只需要等所有狀態(tài)都修改好了之后再進(jìn)行渲染就可以減少一些性能損耗。還有一個(gè)例子,如果更改了N個(gè)狀態(tài),其實(shí)只需要一次setState就可以將DOM更新到最新,如果我們更新多個(gè)值。
this.setState({ msg: 1 });
this.setState({ age: 2 });
this.setState({ name: 3 });此處我們分三次修改了三種狀態(tài),但其實(shí)React只需要渲染一次,在setState批處理之后會(huì)將其合并,并進(jìn)行一次re-render就可以將整個(gè)組件的DOM更新到最新,根本不需要關(guān)心這個(gè)setState到底是從哪個(gè)具體的狀態(tài)發(fā)出來的。
那么還有一個(gè)問題,首先我們可以認(rèn)同進(jìn)行批處理更新對(duì)我們的性能是有益的,例如Child和Parent都調(diào)用setState,我們不需要重新渲染Child兩次。但是此時(shí)我們可能會(huì)想到一個(gè)問題,為什么不能如同Vue一樣,Vue是在值更新之后觸發(fā)setter然后進(jìn)行更新,更新的過程同樣也是采用異步渲染,也會(huì)將所有觸發(fā)Watcher的update進(jìn)行去重合并再去更新視圖,也就是說Vue是立即修改了值,而后再去更新視圖的。也就是說,相比較于React,為什么不能在同樣做批處理的情況下,立即將setState更新寫入this.state而不等待協(xié)調(diào)結(jié)束。
任何一種解決方案都有權(quán)衡,對(duì)于Vue來說因?yàn)槠涫峭ㄟ^劫持了數(shù)據(jù)的setter過程,在使用的也是直接使用=直接賦值的,而在賦值之后進(jìn)行視圖的更新也是一個(gè)自然的過程,如果類似于React一樣在=之后這個(gè)值依然沒有變化,在直覺上是非常不符合常理的,雖然Vue是通過劫持setter實(shí)現(xiàn)的視圖更新,但是做到如同React一樣并不是不可能的,這是Vue采用的解決方案上的權(quán)衡,當(dāng)然這只是可能的一個(gè)理由,說是問題的權(quán)衡,實(shí)際上還是需要對(duì)比React來看,對(duì)于React中要處理的問題,Vue自然會(huì)有自己解決方案的權(quán)衡,歸根到底還是框架的設(shè)計(jì)哲學(xué)的問題。對(duì)于上面提出的在同樣做批處理的情況下,立即將setState更新寫入this.state而不等待協(xié)調(diào)結(jié)束的這個(gè)問題,dan給予了兩個(gè)理由,在此簡作總結(jié),完整的英文版本還請看參考中的github issue。
保證內(nèi)部數(shù)據(jù)統(tǒng)一
即使state是同步更新的,但props是不會(huì)的,在重新渲染父組件之前,無法知道props,如果同步執(zhí)行此操作,批處理就會(huì)消失?,F(xiàn)在React提供的對(duì)象state、props、refs在內(nèi)部是一致的。這意味著如果只使用這些對(duì)象,則可以保證它們引用完全協(xié)調(diào)的樹,即使它是該樹的舊版本。當(dāng)僅使用state時(shí),同步刷新的模式將起作用。
console.log(this.state.value); // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value); // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value); // 2但是,假設(shè)需要提升此狀態(tài)以在幾個(gè)組件之間共享,因此將其移動(dòng)到父級(jí),也就是說有props參與到了傳值,那么同步setState模式就會(huì)有問題,此時(shí)將state提升到了父組件,利用props將值傳導(dǎo)子組件。
console.log(this.props.value); // 0 this.props.onIncrement(); console.log(this.props.value); // 0 this.props.onIncrement(); console.log(this.props.value); // 0
這是因?yàn)樵谶@個(gè)解決方案中,this.state會(huì)立即刷新,而this.props不會(huì),而且我們不能在不重新渲染父對(duì)象的情況下立即刷新this.props,這意味著我們將不得不放棄批處理的策略。還有更微妙的情況說明這如何破壞一致性的,例如這種方案正在混合來自props尚未刷新和state建議立即刷新的數(shù)據(jù)以創(chuàng)建新狀態(tài)。在React中,this.state和this.props都只在協(xié)調(diào)和刷新之后更新,所以你會(huì)在refactoring之前和之后看到0被打印出來。這使得提升狀態(tài)安全。在某些情況下這可能會(huì)帶來不便,特別是對(duì)于來自更多OO背景的人來說,他們只想多次改變狀態(tài),而不是考慮如何在一個(gè)地方表示完整的狀態(tài)更新,我可以理解這一點(diǎn),盡管我確實(shí)認(rèn)為從調(diào)試的角度來看,保持狀態(tài)更新的集中更加清晰??偠灾?,React模型并不總是會(huì)產(chǎn)生最簡潔的代碼,但它在內(nèi)部是一致的,并確保提升狀態(tài)是安全的。
啟用并發(fā)更新
從概念上講React的行為就好像每個(gè)組件都有一個(gè)更新隊(duì)列,我們在這里討論是否同步刷新state有一個(gè)前提那就是我們默認(rèn)更新節(jié)點(diǎn)是遵循特定的順序的,但是按默認(rèn)順序更新組件在以后的react中可能就變了。對(duì)于現(xiàn)在我們一直在談?wù)摰漠惒戒秩?,我承認(rèn)我們在傳達(dá)這意味著什么方面做得不是很好,但這就是研發(fā)的本質(zhì):你追求一個(gè)在概念上看起來很有前途的想法,但只有在花了足夠的時(shí)間之后才能真正理解它的含義。
對(duì)于這個(gè)理由,是React發(fā)展的一個(gè)方向。我們一直在解釋異步渲染的一種方式是React可以根據(jù)setState()調(diào)用的來源分配不同的優(yōu)先級(jí):事件處理程序、網(wǎng)絡(luò)響應(yīng)、動(dòng)畫等。例如你現(xiàn)在正在打字,那么TextBox組件需要實(shí)時(shí)的刷新,但是當(dāng)你在輸入的時(shí)候,來了一個(gè)信息,這個(gè)時(shí)候可能讓信息的渲染延遲到某個(gè)閾值,而不是因?yàn)樽枞€程而讓輸入卡頓。如果我們讓某些更新具有較低優(yōu)先級(jí),我們可以將它們的渲染分成幾毫秒的小塊,這樣用戶就不會(huì)注意到它們。異步rendering不僅僅是性能上的優(yōu)化,我們認(rèn)為這是React組件模型可以做什么的根本性轉(zhuǎn)變。例如,考慮從一個(gè)屏幕導(dǎo)航到另一個(gè)屏幕的情況,通常會(huì)在渲染新屏幕時(shí)顯示一個(gè)導(dǎo)航器,但是如果導(dǎo)航速度足夠快,閃爍并立即隱藏導(dǎo)航器會(huì)導(dǎo)致用戶體驗(yàn)下降,更糟糕的是如果有多個(gè)級(jí)別的組件具有不同的異步依賴項(xiàng)例如數(shù)據(jù)、代碼、圖像等,您最終會(huì)得到一連串短暫閃爍的導(dǎo)航器。由于所有的DOM重排,這既在視覺上令人不快,又使您的應(yīng)用程序在實(shí)踐中變慢。如果當(dāng)您執(zhí)行一個(gè)簡單的setState()來呈現(xiàn)不同的視圖時(shí),我們可以開始在后臺(tái)呈現(xiàn)更新后的視圖。如果您自己不編寫任何協(xié)調(diào)代碼,您可以選擇在更新時(shí)間超過某個(gè)閾值時(shí)顯示導(dǎo)航器,否則當(dāng)整個(gè)新子樹的異步依賴項(xiàng)是時(shí)讓React執(zhí)行無縫轉(zhuǎn)換使?jié)M意。請注意,這只是可能的,因?yàn)?code>this.state不會(huì)立即刷新,如果它被立即刷新,我們將無法開始在后臺(tái)渲染視圖的新版本,而舊版本仍然可見且可交互,他們獨(dú)立的狀態(tài)更新會(huì)發(fā)生沖突。
參考
https://www.jianshu.com/p/cc12e9a8052c
https://juejin.cn/post/6844903636749778958
https://zh-hans.reactjs.org/docs/faq-state.html
https://blog.csdn.net/zz_jesse/article/details/118282921
https://blog.csdn.net/weixin_44874595/article/details/104270057
到此這篇關(guān)于React中setState同步異步場景的使用的文章就介紹到這了,更多相關(guān)React setState同步異步內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React實(shí)現(xiàn)雙滑塊交叉滑動(dòng)
這篇文章主要為大家詳細(xì)介紹了React實(shí)現(xiàn)雙滑塊交叉滑動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
React?UI組件庫之快速實(shí)現(xiàn)antd的按需引入和自定義主題
react入門學(xué)習(xí)告一段路,下面這篇文章主要給大家介紹了關(guān)于React?UI組件庫之快速實(shí)現(xiàn)antd的按需引入和自定義主題的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
React中使用Workbox進(jìn)行預(yù)緩存的實(shí)現(xiàn)代碼
Workbox是Google Chrome團(tuán)隊(duì)推出的一套 PWA 的解決方案,這套解決方案當(dāng)中包含了核心庫和構(gòu)建工具,因此我們可以利用Workbox實(shí)現(xiàn)Service Worker的快速開發(fā),本文小編給大家介紹了React中使用Workbox進(jìn)行預(yù)緩存的實(shí)現(xiàn),需要的朋友可以參考下2023-11-11
使用React+ts實(shí)現(xiàn)無縫滾動(dòng)的走馬燈詳細(xì)過程
這篇文章主要給大家介紹了關(guān)于使用React+ts實(shí)現(xiàn)無縫滾動(dòng)的走馬燈詳細(xì)過程,文中給出了詳細(xì)的代碼示例以及圖文教程,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-08-08
React-Native實(shí)現(xiàn)ListView組件之上拉刷新實(shí)例(iOS和Android通用)
本篇文章主要介紹了React-Native實(shí)現(xiàn)ListView組件之上拉刷新實(shí)例(iOS和Android通用),具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
react-native 完整實(shí)現(xiàn)登錄功能的示例代碼
本篇文章主要介紹了react-native 完整實(shí)現(xiàn)登錄功能的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放時(shí)間滾動(dòng))
這篇文章主要為大家詳細(xì)介紹了React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放使勁按滾動(dòng)),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2024-02-02
React BootStrap用戶體驗(yàn)框架快速上手
這篇文章主要介紹了React BootStrap用戶體驗(yàn)框架快速上手的相關(guān)知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-03-03

