一份完整的useEffect指南(1)

本文太長(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

image

本文假設(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í)一些策略(主要是useReduceruseCallback)斤寂,這些策略可以消除對(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ā)之前
image

你希望彈出框顯示什么厦凤?它會(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' }戚宦。
  • React: 當(dāng)然。更新UI锈嫩。嘿瀏覽器受楼,我正在向DOM添加一些東西。
  • Browser: 很酷呼寸,我把它畫(huà)到了屏幕上艳汽。
  • React: 好的,現(xiàn)在我要運(yùn)行你給我的效果了对雪。
    • 運(yùn)行 () => { document.title = 'You clicked 0 times' }河狐。

現(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' }
  • React: 當(dāng)然捐祠。更新UI碱鳞。嘿瀏覽器,我改變了DOM雏赦。
  • Browser: 很酷劫笙,我把你的更改畫(huà)到了屏幕上。
  • React: 好的星岗,現(xiàn)在我將運(yùn)行屬于我剛剛做的渲染的效果填大。
    • 運(yùn)行 () => { document.title = 'You clicked 1 times' }

每個(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值剖踊。你可以自己試試

image

你可能會(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

image

我認(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);
  });
  // ...
image

在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ò)這里…??

image

引用上一節(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.titlename 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的第二部分 —— 下一篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市存哲,隨后出現(xiàn)的幾起案子因宇,更是在濱河造成了極大的恐慌七婴,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件察滑,死亡現(xiàn)場(chǎng)離奇詭異打厘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)贺辰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)户盯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人魂爪,你說(shuō)我怎么就攤上這事先舷。” “怎么了滓侍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)牲芋。 經(jīng)常有香客問(wèn)我撩笆,道長(zhǎng),這世上最難降的妖魔是什么缸浦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任夕冲,我火速辦了婚禮,結(jié)果婚禮上裂逐,老公的妹妹穿的比我還像新娘歹鱼。我一直安慰自己,他們只是感情好卜高,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布弥姻。 她就那樣靜靜地躺著,像睡著了一般掺涛。 火紅的嫁衣襯著肌膚如雪庭敦。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天薪缆,我揣著相機(jī)與錄音秧廉,去河邊找鬼。 笑死拣帽,一個(gè)胖子當(dāng)著我的面吹牛疼电,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播减拭,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蔽豺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了峡谊?” 一聲冷哼從身側(cè)響起茫虽,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤刊苍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后濒析,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體正什,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年号杏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了婴氮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盾致,死狀恐怖主经,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庭惜,我是刑警寧澤罩驻,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站护赊,受9級(jí)特大地震影響惠遏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骏啰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一节吮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧判耕,春花似錦透绩、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至请毛,卻和暖如春志鞍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背方仿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工固棚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仙蚜。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓此洲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親委粉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呜师,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355