本文太長(zhǎng)亏镰,謹(jǐn)慎閱讀撤蟆。
本文原文:一份完整的useEffect指南
原文檔: A Complete Guide to useEffect
你使用hooks寫(xiě)了一些組件漆腌。也許只是一個(gè)小的應(yīng)用簿透。你基本上是滿(mǎn)意的姥饰。你對(duì)這個(gè)API游刃有余羹令,并在此過(guò)程中學(xué)會(huì)了一些技巧鲤屡。你甚至做了一些自定義Hook來(lái)提取重復(fù)邏輯(300行代碼消失了!)并向你的同事展示了它福侈。他們會(huì)說(shuō)酒来,“干得好”。
但是有時(shí)候當(dāng)你使用useEffect
的時(shí)候肪凛,有些部分并不完全合適堰汉。你有一種揮之不去的感覺(jué),覺(jué)得自己錯(cuò)過(guò)了什么显拜。它似乎與類(lèi)生命周期相似......但它真的如此嗎衡奥?你發(fā)現(xiàn)自己會(huì)問(wèn)這樣的問(wèn)題:
- ?? 如何使用
useEffect
替代componentDidMount
? - ?? 如何在
useEffect
中正確的獲取數(shù)據(jù)远荠?[]
是什么矮固? - ?? 我是否需要將函數(shù)指定為效果(effect)依賴(lài)項(xiàng)?
- ?? 為什么有時(shí)會(huì)得到一個(gè)無(wú)限重復(fù)獲取循環(huán)?
- ?? 為什么我有時(shí)會(huì)在效果(effect)中獲得舊的狀態(tài)或prop值?
當(dāng)我剛剛開(kāi)始使用Hooks時(shí)譬淳,我也對(duì)所有這些問(wèn)題感到困惑档址。即使在寫(xiě)最初的文檔時(shí),我也沒(méi)有對(duì)其中的一些細(xì)節(jié)有一個(gè)很好的把握邻梆。從那以后守伸,我有一些“啊哈”(豁然開(kāi)朗)的時(shí)刻,我想和你們分享浦妄。這個(gè)的深入研究將使這些問(wèn)題的答案對(duì)你來(lái)說(shuō)顯而易見(jiàn)尼摹。
為了看 到答案见芹,我們需要后退一步。本文的目的不是給你一個(gè)方法列表去用蠢涝。主要是為了幫助你真正“理解”useEffect
玄呛。沒(méi)有太多要學(xué)的東西。事實(shí)上和二,我們需要花費(fèi)大部分時(shí)間去忘記學(xué)習(xí)的東西(unlearning)徘铝。
直到我不再通過(guò)熟悉的類(lèi)生命周期方法來(lái)看待useEffect
Hook之后, 這一切才融會(huì)貫通。
"忘掉你學(xué)到的東西"惯吕√杷— Yoda
本文假設(shè)你對(duì)useEffectAPI有點(diǎn)熟悉。
本文的確很長(zhǎng)废登。就像一本迷你小書(shū)淹魄。這只是我喜歡的版式。但是如果你很忙或者不在乎钳宪,我在下面寫(xiě)了一個(gè)TLDR供你快速了解揭北。
如果你對(duì)深度學(xué)習(xí)不怎么喜歡扳炬,你可能想等到這些解釋出現(xiàn)在其他地方再學(xué)習(xí)。就像React在2013年推出時(shí)一樣,人們需要一段時(shí)間才能認(rèn)識(shí)到一種不同的思維模式并進(jìn)行教學(xué)剧腻。
TLDR
如果你不想閱讀整篇文章幽纷,這里有一個(gè)快速的TLDR。如果有些部分沒(méi)有意義劝术,可以向下滾動(dòng)缩多,直到找到相關(guān)的內(nèi)容。
如果你打算閱讀整篇文章养晋,可以跳過(guò)它衬吆。我會(huì)在最后鏈接到它。
?? 問(wèn)題:如何使用useEffect
替代componentDidMount
绳泉?
雖然可以用useEffect(fn, [])
逊抡,但它并不是完全等價(jià)的。與componentDidMount
不同零酪,它將捕獲(capture) props和狀態(tài)冒嫡。所以即使在回調(diào)中,你也會(huì)看到最初的props和狀態(tài)四苇。如果你想看到“最新”的東西孝凌,你可以把它寫(xiě)到ref。但是通常有一種更簡(jiǎn)單的方法來(lái)構(gòu)造代碼月腋,這樣你就不必這樣做了蟀架。請(qǐng)記住瓣赂,效果(effect)的心理模型與componentDidMount
和其他生命周期不同,試圖找到它們的確切對(duì)等物可能會(huì)讓你感到困惑片拍,而不是有所幫助钩述。為了提高效率,你需要“思考效果”穆碎,他們的心智模型更接近于實(shí)現(xiàn)同步牙勘,而不是響應(yīng)生命周期事件。
?? 問(wèn)題:如何在useEffect
中正確獲取數(shù)據(jù)所禀?什么是 []
方面?
這篇文章是使用useEffect
進(jìn)行數(shù)據(jù)獲取的一個(gè)很好的入門(mén)讀物。一定要把它讀完色徘!它不像這個(gè)那么長(zhǎng)恭金。[]
表示該效果不使用參與React數(shù)據(jù)流的任何值,因此可以安全地應(yīng)用一次褂策。當(dāng)實(shí)際使用該值時(shí)横腿,它也是一個(gè)常見(jiàn)的bug源。你需要學(xué)習(xí)一些策略(主要是useReducer
和useCallback
)斤寂,這些策略可以消除對(duì)依賴(lài)項(xiàng)的需要耿焊,而不是錯(cuò)誤地忽略它。
?? 問(wèn)題:我是否需要將函數(shù)指定為效果依賴(lài)項(xiàng)遍搞?
建議將不需要props或狀態(tài)的函數(shù)提到組件外部罗侯,并將僅由效果內(nèi)部使用的函數(shù)拉出來(lái)使用。如果在此之后溪猿,你的效果仍然使用渲染范圍中的函數(shù)(包括來(lái)自props的函數(shù))钩杰,那么將它們封裝到定義它們的useCallback
中,并重復(fù)該過(guò)程诊县。為什么這很重要?函數(shù)可以“看到”來(lái)自props和狀態(tài)的值——因此它們參與數(shù)據(jù)流讲弄。我們的常見(jiàn)問(wèn)題解答中有更詳細(xì)的答案。
?? 問(wèn)題:為什么有時(shí)會(huì)得到一個(gè)無(wú)限重復(fù)獲取循環(huán)?
如果在沒(méi)有第二個(gè)依賴(lài)項(xiàng)參數(shù)的情況下在一個(gè)效果中執(zhí)行數(shù)據(jù)獲取依痊,則可能會(huì)發(fā)生這種情況避除。沒(méi)有它,效果會(huì)在每次渲染后運(yùn)行 - 設(shè)置狀態(tài)將再次觸發(fā)效果抗悍。如果指定的這個(gè)值始終在依賴(lài)關(guān)系數(shù)組中改變驹饺,也可能發(fā)生無(wú)限循環(huán)。你可以通過(guò)一個(gè)一個(gè)地把它們移除來(lái)辨別是哪個(gè)缴渊。但是赏壹,刪除你使用的依賴(lài)項(xiàng)(或盲目指定[]
)通常是錯(cuò)誤的修復(fù)。相反衔沼,從源頭解決問(wèn)題蝌借。例如昔瞧,函數(shù)可能會(huì)導(dǎo)致這個(gè)問(wèn)題,將它們放在效果中菩佑、將它們提出來(lái)或使用useCallback
包裝它們會(huì)有所幫助自晰。為避免重新創(chuàng)建對(duì)象,useMemo
可用于類(lèi)似目的稍坯。
?? 為什么我有時(shí)會(huì)在效果中獲得舊的狀態(tài)或props值酬荞?
效果總是從定義的渲染中“看到”props和狀態(tài)。這有助于防止錯(cuò)誤瞧哟,但在某些情況下可能會(huì)令人討厭混巧。對(duì)于這些情況,你可以顯式地在可變r(jià)ef中維護(hù)一些值(鏈接里的文章在最后對(duì)此進(jìn)行了解釋)勤揩。如果你認(rèn)為你從舊的渲染中看到了一些props或狀態(tài)咧党,但并不是你期望的,那么你可能錯(cuò)過(guò)了一些依賴(lài)項(xiàng)陨亡。嘗試使用lint規(guī)則來(lái)鍛煉自己發(fā)現(xiàn)它們傍衡。過(guò)幾天,它就會(huì)成為你的第二天性负蠕。請(qǐng)參閱常見(jiàn)問(wèn)題解答中的此答案蛙埂。
我希望這個(gè)TLDR很有幫助!否則虐急,我們繼續(xù)吧箱残。
每個(gè)渲染都有自己的props和狀態(tài)
在我們談?wù)撔Ч疤下酰覀冃枰懻撲秩尽?/p>
這是一個(gè)counter止吁。仔細(xì)查看突出顯示的行:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
+ <p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
這是什么意思?是否count
以某種方式“監(jiān)視”狀態(tài)的更改并自動(dòng)更新? 當(dāng)你學(xué)習(xí)React時(shí),這可能是一個(gè)有用的第一直覺(jué)燎悍,但它不是一個(gè)準(zhǔn)確的心理模型敬惦。
在此示例中,count
只是一個(gè)數(shù)字谈山。 它不是神奇的“數(shù)據(jù)綁定”俄删,“觀察者”,“代理”或其他任何東西奏路。這是一個(gè)很古老的數(shù)字:
const count = 42;
// ...
<p>You clicked {count} times</p>
// ...
我們的組件第一次渲染時(shí)畴椰,我們從useState()
得到的count
變量是0
。當(dāng)我們調(diào)用setCount(1)
時(shí)鸽粉,React再次調(diào)用我們的組件斜脂。這一次,count
將是1
触机。依此類(lèi)推:
// During first render
function Counter() {
const count = 0; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// After a click, our function is called again
function Counter() {
const count = 1; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// After another click, our function is called again
function Counter() {
const count = 2; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
每當(dāng)我們更新?tīng)顟B(tài)時(shí)帚戳,React都會(huì)調(diào)用我們的組件玷或。每個(gè)渲染結(jié)果“看到”它自己的counter
狀態(tài)值,這是我們函數(shù)內(nèi)的常量片任。
所以這一行沒(méi)有做任何特殊的數(shù)據(jù)綁定:
<p>You clicked {count} times</p>
它只是在渲染輸出中嵌入一個(gè)數(shù)值偏友。 該數(shù)字由React提供。當(dāng)我們setCount
時(shí)对供,React再次使用不同的count
值調(diào)用我們的組件位他。然后React更新DOM以匹配我們最新的渲染輸出。
關(guān)鍵是产场,任何特定渲染中的count
常量都不會(huì)隨時(shí)間變化棱诱。再次調(diào)用的是我們的組件——每個(gè)渲染都“看到”自己的count
值,該值在渲染之間是獨(dú)立的涝动。
(有關(guān)此過(guò)程的深入概述迈勋,請(qǐng)查看我的帖子React作為UI運(yùn)行時(shí)。)
每個(gè)渲染都有自己的事件處理程序
到現(xiàn)在為止還挺好醋粟。那么關(guān)于事件處理程序是怎樣的靡菇?
看看這個(gè)例子。它會(huì)在三秒鐘后顯示count
的彈框:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
假設(shè)我執(zhí)行以下步驟:
- 增加 計(jì)數(shù)到3
- 點(diǎn)擊 “Show alert”
- 增加 到5米愿,在timeout觸發(fā)之前
你希望彈出框顯示什么厦凤?它會(huì)顯示5 - 這是彈框時(shí)的計(jì)數(shù)器狀態(tài)嗎?或者它會(huì)顯示3 - 我點(diǎn)擊時(shí)的狀態(tài)育苟?
劇透
來(lái)吧较鼓,[親自嘗試一下](try it yourself!)!
如果這種行為對(duì)你沒(méi)有意義违柏,想象一個(gè)更實(shí)際的例子:一個(gè)聊天應(yīng)用程序博烂,當(dāng)前收件人ID在狀態(tài)中,并有一個(gè)發(fā)送按鈕漱竖。這篇文章深入探討了原因禽篱,但正確的答案是3。
彈框(alert)將在我單擊按鈕時(shí)“捕獲”狀態(tài)馍惹。
(有一些方法可以實(shí)現(xiàn)其他行為躺率,但我現(xiàn)在將關(guān)注默認(rèn)情況。當(dāng)我們建立一個(gè)心理模型時(shí)万矾,重要的是我們要區(qū)分“最小阻力路徑”和選擇進(jìn)入逃生艙悼吱。)
但它是如何工作的?
我們已經(jīng)討論過(guò)count
值對(duì)于我們函數(shù)的每個(gè)特定調(diào)用都是常量良狈。值得強(qiáng)調(diào)的是這一點(diǎn) — 我們的函數(shù)被調(diào)用了很多次(每次渲染一次)后添,但是每次調(diào)用的count
值都是常量,并被設(shè)置為一個(gè)特定的值(該渲染的狀態(tài))们颜。
這不是React所特有的 - 常規(guī)函數(shù)以類(lèi)似的方式工作:
function sayHi(person) {
const name = person.name; setTimeout(() => {
alert('Hello, ' + name);
}, 3000);
}
let someone = {name: 'Dan'};
sayHi(someone);
someone = {name: 'Yuzhi'};
sayHi(someone);
someone = {name: 'Dominic'};
sayHi(someone);
在此示例中吕朵,外部someone
變量被重新分配多次猎醇。(就像React中的某個(gè)地方一樣,當(dāng)前 的組件狀態(tài)可以改變努溃。) 但是硫嘶,在sayHi
中,有一個(gè)本地name
常量與特定調(diào)用中的person
相關(guān)聯(lián)梧税。 那個(gè)常量是本地的沦疾,所以它在調(diào)用之間是獨(dú)立的!因此第队,當(dāng)超時(shí)觸發(fā)時(shí)哮塞,每個(gè)彈框都會(huì)“記住”他自己的name
。
這解釋了我們的事件處理程序如何在單擊時(shí)捕獲count
凳谦。如果我們應(yīng)用相同的替換原則忆畅,每個(gè)渲染“看到”它自己的count
:
// During first render
function Counter() {
+ const count = 0; // Returned by useState() // ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// After a click, our function is called again
function Counter() {
+ const count = 1; // Returned by useState() // ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// After another click, our function is called again
function Counter() {
+ const count = 2; // Returned by useState() // ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
如此有效,每個(gè)渲染都返回自己“版本”的handleAlertClick
尸执。每個(gè)版本都“記住”自己的count
:
// During first render
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
+ alert('You clicked on: ' + 0);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 0 inside
// ...
}
// After a click, our function is called again
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
+ alert('You clicked on: ' + 1);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 1 inside
// ...
}
// After another click, our function is called again
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
+ alert('You clicked on: ' + 2);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 2 inside
// ...
}
這就是為什么在這個(gè)演示中 事件處理程序“屬于”特定渲染家凯,當(dāng)你點(diǎn)擊時(shí),它會(huì)繼續(xù)使用 該 渲染中的counter
狀態(tài)如失。
在任何特定渲染中绊诲,props和狀態(tài)永遠(yuǎn)保持不變。 但是褪贵,如果在渲染之間隔離了props和狀態(tài)掂之,那么使用它們的任何值(包括事件處理程序)都是獨(dú)立的。它們也“屬于”特定的渲染脆丁。因此世舰,即使事件處理程序中的異步函數(shù)也會(huì)“看到”相同的count
值。
旁注:我將具體count
值內(nèi)聯(lián)到上面的handleAlertClick
函數(shù)中偎快。這種心理替代是安全的冯乘,因?yàn)?code>count不可能在特定渲染中改變。它被聲明為const
并且是一個(gè)數(shù)字晒夹。以同樣的方式考慮其他值(比如對(duì)象)是安全的,但前提是我們同意避免狀態(tài)的變化姊氓。使用新創(chuàng)建的對(duì)象調(diào)用setSomething(newObj)
而不是對(duì)其進(jìn)行修改丐怯,因?yàn)閷儆谝郧颁秩镜臓顟B(tài)是完整的。
每個(gè)渲染都有自己的效果(effects)
這應(yīng)該是一個(gè)關(guān)于效果的論述翔横,但我們還沒(méi)有談到效果!我們現(xiàn)在要糾正這個(gè)問(wèn)題读跷。事實(shí)證明,效果并沒(méi)有什么不同禾唁。
讓我們回到文檔中的一個(gè)例子:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
這里有一個(gè)問(wèn)題:效果如何讀取最新的count
狀態(tài)效览?
也許无切,有某種“數(shù)據(jù)綁定”或“觀查”使得count
更新在效果函數(shù)中生效?也許count
是一個(gè)可變變量丐枉,React在我們的組件中設(shè)置哆键,以便我們的效果總能看到最新值?
不瘦锹。
我們已經(jīng)知道count
在特定組件渲染中是恒定(常量)的籍嘹。事件處理程序從渲染中“查看”它們“所屬”的count
狀態(tài),因?yàn)?code>count是它們作用域中的一個(gè)變量弯院。效果也是如此辱士!
count
變量不是以某種方式在“不變”效果中發(fā)生變化。效果函數(shù)本身 在每個(gè)渲染上都是不同的听绳。**
每個(gè)版本從其“所屬”的渲染中“看到”count
值:
// During first render
function Counter() {
// ...
useEffect(
// 主要是看這里
// Effect function from first render
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// After a click, our function is called again
function Counter() {
// ...
useEffect(
// 主要是看這里
// Effect function from second render
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
// After another click, our function is called again
function Counter() {
// ...
useEffect(
// 主要是看這里
// Effect function from third render
() => {
document.title = `You clicked ${2} times`;
}
);
// ..
}
React會(huì)記住你提供的效果函數(shù)颂碘,刷新對(duì)DOM的更改并讓瀏覽器繪制到屏幕后運(yùn)行該函數(shù)。
因此椅挣,即使我們?cè)谶@里談到單一概念的效果(示例中的更新文檔標(biāo)題(document.title=''
))凭涂,它也會(huì)在每個(gè)渲染上用不同的函數(shù) 表示 — 并且每個(gè)效果函數(shù)從它所屬的特定渲染中“看到”props和狀態(tài)。
從概念上講贴妻,你可以想象效果是 渲染結(jié)果的一部分 切油。
嚴(yán)格地說(shuō),它們不是(為了允許Hook組合而沒(méi)有笨拙的語(yǔ)法或運(yùn)行時(shí)開(kāi)銷(xiāo))名惩。但是在我們構(gòu)建的心理模型中澎胡,效果函數(shù)屬于特定的渲染,就像事件處理程序一樣娩鹉。
為了確保我們有一個(gè)扎實(shí)的理解攻谁,讓我們回顧一下我們的第一次渲染:
-
React: 給我一個(gè)當(dāng)狀態(tài)為
0
時(shí)UI。 -
你的組件:
- 這是渲染結(jié)果:
<p>You clicked 0 times</p>
弯予。 - 還記得在完成后運(yùn)行此效果:
() => { document.title = 'You clicked 0 times' }
戚宦。
- 這是渲染結(jié)果:
- React: 當(dāng)然。更新UI锈嫩。嘿瀏覽器受楼,我正在向DOM添加一些東西。
- Browser: 很酷呼寸,我把它畫(huà)到了屏幕上艳汽。
-
React: 好的,現(xiàn)在我要運(yùn)行你給我的效果了对雪。
- 運(yùn)行
() => { document.title = 'You clicked 0 times' }
河狐。
- 運(yùn)行
現(xiàn)在讓我們回顧點(diǎn)擊后發(fā)生的事情:
-
你的組件: 嘿React,把我的狀態(tài)設(shè)置為
1
。 -
React: 給我狀態(tài)為
1
時(shí)的UI馋艺。 -
你的組件:
- 這是渲染結(jié)果:
<p>You clicked 1 times</p>
栅干。 - 還記得在完成后運(yùn)行此效果:
() => { document.title = 'You clicked 1 times' }
。
- 這是渲染結(jié)果:
- React: 當(dāng)然捐祠。更新UI碱鳞。嘿瀏覽器,我改變了DOM雏赦。
- Browser: 很酷劫笙,我把你的更改畫(huà)到了屏幕上。
-
React: 好的星岗,現(xiàn)在我將運(yùn)行屬于我剛剛做的渲染的效果填大。
- 運(yùn)行
() => { document.title = 'You clicked 1 times' }
。
- 運(yùn)行
每個(gè)渲染都有它自己......一切
我們現(xiàn)在知道在每次渲染之后運(yùn)行的效果在概念上是組件輸出的一部分俏橘,并且“看到”來(lái)自該特定渲染的props和狀態(tài)允华。
我們來(lái)試試吧×绕考慮以下代碼:
function Counter() {
const [count, setCount] = useState(0);
// 手動(dòng)高亮
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
如果我稍微點(diǎn)擊幾次靴寂,那么log會(huì)是什么樣子?
劇透
你可能認(rèn)為這是一個(gè)問(wèn)題召耘,最終結(jié)果是不直觀的百炬。不是這樣的!我們將看到一系列日志 - 每個(gè)日志屬于特定的渲染污它,因此具有自己的count
值剖踊。你可以自己試試:
你可能會(huì)想:“當(dāng)然,這就是它的工作原理衫贬!不然它還能怎樣工作德澈?“
好吧,這不是this.state
在類(lèi)上的運(yùn)作方式固惯。很容易錯(cuò)誤地認(rèn)為這個(gè)類(lèi)的實(shí)現(xiàn)是等價(jià)的:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
但是梆造,this.state.count
始終指向最新計(jì)數(shù)而不是屬于特定渲染的計(jì)數(shù)。所以你會(huì)看到每次記錄5
:
我認(rèn)為具有諷刺意味的是葬毫,Hook嚴(yán)重依賴(lài)于JavaScript閉包镇辉,然而正是類(lèi)實(shí)現(xiàn)遭受了典型的超時(shí)錯(cuò)誤值的混淆,而這種混淆通常與閉包相關(guān)供常。這是因?yàn)楸纠谢煜膶?shí)際來(lái)源是突變(react使類(lèi)中的this.state
發(fā)生突變摊聋,以指向最新?tīng)顟B(tài)),而不是閉包本身栈暇。
當(dāng)你相關(guān)的值永遠(yuǎn)不變時(shí),閉包是非常棒的箍镜。這使得它們易于思考源祈,因?yàn)槟銓?shí)質(zhì)上是指向常量煎源。 正如我們所討論的,props和狀態(tài)永遠(yuǎn)不會(huì)在特定渲染中發(fā)生變化香缺。順便說(shuō)一句手销,我們可以通過(guò)使用閉包修復(fù)類(lèi)版本中的問(wèn)題。
逆勢(shì)而行(逆流而上)(Swimming Against the Tide)
在這一點(diǎn)上图张,我們明確地稱(chēng)之為:組件渲染中的 每個(gè) 函數(shù)(包括事件處理程序锋拖、效果、超時(shí)或其中的API調(diào)用)都會(huì)捕獲定義它的渲染調(diào)用的props和狀態(tài)祸轮。
所以這兩個(gè)例子是等價(jià)的:
function Example(props) {
useEffect(() => {
setTimeout(() => {
+ console.log(props.counter); }, 1000);
});
// ...
}
function Example(props) {
+ const counter = props.counter;
useEffect(() => {
setTimeout(() => {
+ console.log(counter); }, 1000);
});
// ...
}
無(wú)論你是否在組件內(nèi)部從props或狀態(tài)中"提前"讀取都無(wú)關(guān)緊要兽埃。 他們不會(huì)改變!在單個(gè)渲染的作用域內(nèi)适袜,props和狀態(tài)保持不變柄错。 (解構(gòu)props使這更加明顯。)
當(dāng)然苦酱,有時(shí)你希望在效果中定義的某些回調(diào)中讀取最新而非捕獲的值售貌。最簡(jiǎn)單的方法是使用refs,如本文最后一節(jié)所述疫萤。
注意颂跨,當(dāng)你想從 過(guò)去 渲染的函數(shù)中讀取 未來(lái) 的props或狀態(tài)時(shí),你是在逆潮流而上扯饶。這并沒(méi)有錯(cuò)(在某些情況下是必要的)恒削,但打破這種模式可能看起來(lái)不那么“干凈”。這是一個(gè)有意的結(jié)果帝际,因?yàn)樗兄谕怀瞿男┐a是脆弱的蔓同,并且依賴(lài)于時(shí)間而改變。在類(lèi)上蹲诀,發(fā)生這種情況時(shí)不太明顯斑粱。
這是我們的計(jì)數(shù)器示例的一個(gè)版本,它復(fù)制了類(lèi)的行為:
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
在React中改變一些東西似乎很古怪脯爪。但是则北,這正是React本身在類(lèi)中重新分配this.state
的方式。與捕獲的props和state不同痕慢,你無(wú)法保證讀取latestCount.current
會(huì)在任何特定回調(diào)中為你提供相同的值尚揣。根據(jù)定義,你可以隨時(shí)改變它掖举。這就是為什么它不是默認(rèn)值快骗,你必須選擇它。
那么清理呢?
正如文檔所解釋的那樣,某些effect可能會(huì)有一個(gè)清理階段方篮。本質(zhì)上名秀,它的目的是“撤消”訂閱等情況的效果。
考慮以下代碼:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
假設(shè)props
在第一個(gè)渲染時(shí)為{id:10}
藕溅,在第二個(gè)渲染上為{id:20}
匕得。你 可能 認(rèn)為會(huì)發(fā)生這樣的事情:
- React清除
{id:10}
的效果。 - React為
{id:20}
渲染UI巾表。 - React運(yùn)行
{id:20}
的效果汁掠。
(情況并非如此。)
使用這個(gè)心理模型集币,你可能認(rèn)為清理“看到”了舊的props考阱,因?yàn)樗谖覀冎匦落秩局斑\(yùn)行,然后新的效果“看到”了新props惠猿,因?yàn)樗谥匦落秩局筮\(yùn)行羔砾。這是直接從類(lèi)生命周期中提取的心智模型,這里并不準(zhǔn)確 偶妖。讓我們看看這是為什么姜凄。
React僅在讓瀏覽器繪制后運(yùn)行效果。這使你的應(yīng)用程序更快趾访,因?yàn)榇蠖鄶?shù)效果不需要阻止屏幕更新态秧。效果清理也會(huì)延遲。使用新props重新渲染后扼鞋,前一個(gè)效果會(huì)被清除:**
- React為
{id:20}
渲染UI申鱼。 - 瀏覽器繪制畫(huà)面。我們?cè)谄聊簧峡吹搅?code>{id:20}的用戶(hù)界面云头。
- React清除
{id:10}
的效果捐友。 - React運(yùn)行
{id:20}
的效果。
你可能想知道:但是如果在props變?yōu)?code>{id:20}之后運(yùn)行溃槐,那么前一效果的清理如何仍能“看到”舊的{id:10}
props匣砖?
我們以前來(lái)過(guò)這里…??
引用上一節(jié):
組件渲染中的 每個(gè) 函數(shù)(包括事件處理程序、效果昏滴、超時(shí)或其中的API調(diào)用)都會(huì)捕獲定義它的渲染調(diào)用的props和狀態(tài)猴鲫。
現(xiàn)在答案很明確!不管這意味著什么谣殊,效果清理不會(huì)讀取“最新”的props拂共。它讀取屬于它定義的渲染的props:
// First render, props are {id: 10}
function Example() {
// ...
useEffect(
// Effect from first render
() => {
ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
// Cleanup for effect from first render
// 嘿,你的關(guān)注點(diǎn)放在這里(手動(dòng)高亮)
return () => {
ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
};
}
);
// ...
}
// Next render, props are {id: 20}
function Example() {
// ...
useEffect(
// Effect from second render
() => {
ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
// Cleanup for effect from second render
return () => {
ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
};
}
);
// ...
}
王國(guó)將會(huì)升起并化為灰燼姻几,太陽(yáng)將會(huì)褪去它的外層成為一顆白矮星宜狐,最后的文明將會(huì)終結(jié)势告。但是除了{id:10}
之外,沒(méi)有任何東西可以使第一個(gè)渲染效果的清理“看到”props肌厨。
這就是讓React在繪制后立即處理效果的原因——默認(rèn)情況下讓你的應(yīng)用程序運(yùn)行得更快培慌。如果我們的代碼需要它們豁陆,舊的props仍然存在柑爸。
同步,而不是生命周期
關(guān)于React盒音,我最喜歡的一點(diǎn)是它統(tǒng)一了對(duì)初始渲染結(jié)果和更新的描述表鳍。這會(huì)使你的程序降低熵。
假設(shè)我的組件是這樣的:
function Greeting({ name }) {
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
如果我渲染<Greeting name ="Dan"/>
并且后面加上<Greeting name ="Yuzhi"/>
祥诽,或者我只渲染<Greeting name ="Yuzhi"/>
譬圣,都無(wú)關(guān)緊要。最后雄坪,我們將在兩種情況下看到“Hello, Yuzhi”厘熟。
人們常說(shuō):“重要的是過(guò)程,而不是目的地维哈∩蹋”而對(duì)于React,情況正好相反阔挠。重要的是目的地飘庄,而不是旅程。 這就是jQuery代碼中的$.addClass
和$.removeClass
調(diào)用之間(我們的“旅程”)的區(qū)別购撼,并指定了在React代碼中CSS類(lèi)應(yīng)該是 什么(我們的“目的地”)跪削。
React根據(jù)我們當(dāng)前的props和狀態(tài)同步DOM。 渲染時(shí)“mount”或“update”之間沒(méi)有區(qū)別迂求。
你應(yīng)該以類(lèi)似的方式思考效果碾盐。useEffect
允許你根據(jù)我們的props和狀態(tài)同步React樹(shù)之外的東西。**
function Greeting({ name }) {
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
這與熟悉的 mount/update/unmount 心理模型略有不同揩局。將其內(nèi)在化是非常重要的毫玖。如果你試圖編寫(xiě)一個(gè)根據(jù)組件是否第一次渲染而表現(xiàn)不同的效果,那么你就是在逆流而上! 如果我們的結(jié)果取決于“旅程”而不是“目的地”谐腰,我們就無(wú)法同步孕豹。
無(wú)論我們是使用props A,B和C渲染十气,還是使用C立即渲染励背,都無(wú)關(guān)緊要。雖然可能會(huì)有一些暫時(shí)的差異(例如砸西,在獲取數(shù)據(jù)時(shí))叶眉,但最終的結(jié)果應(yīng)該是相同的址儒。
當(dāng)然,在每個(gè)渲染上運(yùn)行所有效果可能并不是很高效衅疙。(在某些情況下莲趣,這會(huì)導(dǎo)致無(wú)限循環(huán)。)
那我們?cè)趺唇鉀Q這個(gè)問(wèn)題呢饱溢?
教React去Diff你的Effects
我們已經(jīng)從DOM本身吸取了教訓(xùn)喧伞。React只更新DOM中實(shí)際發(fā)生更改的部分,而不是在每次重新渲染時(shí)都修改它绩郎。
當(dāng)你更新
<h1 className="Greeting">
Hello, Dan
</h1>
到
<h1 className="Greeting">
Hello, Yuzhi
</h1>
React看到兩個(gè)對(duì)象:
const oldProps = {className: 'Greeting', children: 'Hello, Dan'};
const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};
它檢查它們的每個(gè)props潘鲫,并確定子元素已經(jīng)更改,需要DOM更新肋杖,但className
沒(méi)有溉仑。所以它可以這樣做:
domNode.innerText = 'Hello, Yuzhi';
// No need to touch domNode.className
我們可以用效果做這樣的事嗎?如果不需要應(yīng)用效果状植,最好避免重新運(yùn)行它們浊竟。
例如,由于狀態(tài)更改津畸,我們的組件可能會(huì)重新渲染:
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
{/* 你的關(guān)注點(diǎn)在這里 */}
{/* 原文此處是count, 實(shí)則為counter */}
<button onClick={() => setCounter(count + 1)}>
Increment
</button>
</h1>
);
}
但是我們的效果不使用counter
狀態(tài)振定。我們的效果將document.title
與name
prop同步,但name
prop是相同的洼畅。 在每次計(jì)數(shù)器更改時(shí)重新分配document.title
似乎并不理想吩案。
好吧,那么React會(huì)不會(huì)只是...差異效果(diff effects)?
let oldEffect = () => { document.title = 'Hello, Dan'; };
let newEffect = () => { document.title = 'Hello, Dan'; };
// Can React see these functions do the same thing?
并不是的帝簇。React無(wú)法在不調(diào)用該函數(shù)的情況下猜測(cè)該函數(shù)的功能徘郭。(源代碼實(shí)際上并不包含特定的值,它只是在name
prop上結(jié)束丧肴。)
這就是為什么如果你想避免不必要地重新運(yùn)行效果残揉,你可以為useEffect
提供一個(gè)依賴(lài)數(shù)組(也稱(chēng)為“deps”)參數(shù):
seEffect(() => {
document.title = 'Hello, ' + name;
}, [name]); // Our deps
就像我們告訴React一樣:“嘿,我知道你看不到這個(gè)函數(shù)內(nèi)部芋浮,但我保證它只使用name
而不是渲染作用域中的任何其他內(nèi)容抱环。”**
如果這個(gè)效果在當(dāng)前和上次之間的每個(gè)值都相同纸巷,則無(wú)法同步镇草,因此React可以跳過(guò)效果:
const oldEffect = () => { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];
const newEffect = () => { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];
如果甚至依賴(lài)項(xiàng)數(shù)組中的一個(gè)值在渲染之間是不同的,我們知道運(yùn)行效果不能被跳過(guò)瘤旨。同步所有的東西梯啤!
完整的useEffect的第二部分 —— 下一篇