閱讀 facebook大佬:Dan Abramov 的文章很有感悟javascript
大佬 github地址 https://github.com/gaearonhtml
useEffect
是同步的props
和 state
useRef
獲取改變後的 props
和 state
[]
不能欺騙useReducer
useCallback
設置依賴useMemo
讓複雜對象作動態改變但有時候當你使用 useEffect
你總以爲哪兒有點不對勁。你會嘀咕你可能遺漏了什麼。它看起來像class的生命週期...但真的是這樣嗎?你發覺本身在問相似下面的這些問題:前端
useEffect
模擬 componentDidMount
生命週期?useEffect
裏請求數據? []
又是什麼?當我再也不透過熟悉的class生命週期方法去窺視 useEffect
這個Hook的時候,我才得以融會貫通。java
"忘記你已經學到的。" — Yodareact
若是你打算閱讀整篇文章,你徹底能夠跳過這部分。我會在文章末尾帶上摘要的連接。ios
🤔 Question: 如何用 useEffect
模擬 componentDidMount
生命週期?git
雖然可使用 useEffect(fn, [])
,但它們並不徹底相等。和 componentDidMount
不同, useEffect
會 捕獲 props和state。因此即使在回調函數裏,你拿到的仍是初始的props和state。若是你想獲得"最新"的值,你可使用ref。不過,一般會有更簡單的實現方式,因此你並不必定要用ref。記住,effects的心智模型和 componentDidMount
以及其餘生命週期是不一樣的,試圖找到它們之間徹底一致的表達反而更容易使你混淆。想要更有效,你須要"think in effects",它的心智模型更接近於實現狀態同步,而不是響應生命週期事件。github
🤔 Question: 如何正確地在 useEffect
裏請求數據? []
又是什麼?redux
這篇文章 是很好的入門,介紹瞭如何在 useEffect
裏作數據請求。請務必讀完它!它沒有個人這篇這麼長。 []
表示effect沒有使用任何React數據流裏的值,所以該effect僅被調用一次是安全的。 []
一樣也是一類常見問題的來源,也即你覺得沒使用數據流裏的值但其實使用了。你須要學習一些策略(主要是 useReducer
和 useCallback
)來移除這些effect依賴,而不是錯誤地忽略它們。axios
🤔 Question: 我應該把函數當作effect的依賴嗎?
通常建議把不依賴props和state的函數提到你的組件外面,而且把那些僅被effect使用的函數放到effect裏面。若是這樣作了之後,你的effect仍是須要用到組件內的函數(包括經過props傳進來的函數),能夠在定義它們的地方用 useCallback
包一層。爲何要這樣作呢?由於這些函數能夠訪問到props和state,所以它們會參與到數據流中。咱們官網的FAQ有更詳細的答案。
🤔 Question: 爲何有時候會出現無限重複請求的問題?
這個一般發生於你在effect裏作數據請求而且沒有設置effect依賴參數的狀況。沒有設置依賴,effect會在每次渲染後執行一次,而後在effect中更新了狀態引發渲染並再次觸發effect。無限循環的發生也多是由於你設置的依賴老是會改變。你能夠經過一個一個移除的方式排查出哪一個依賴致使了問題。可是,移除你使用的依賴(或者盲目地使用 []
)一般是一種錯誤的解決方式。你應該作的是解決問題的根源。舉個例子,函數可能會致使這個問題,你能夠把它們放到effect裏,或者提到組件外面,或者用 useCallback
包一層。 useMemo
能夠作相似的事情以免重複生成對象。
🤔 爲何有時候在effect裏拿到的是舊的state或prop呢?
Effect拿到的老是定義它的那次渲染中的props和state。這可以避免一些bugs,但在一些場景中又會有些討人嫌。對於這些場景,你能夠明確地使用可變的ref保存一些值(上面文章的末尾解釋了這一點)。若是你以爲在渲染中拿到了一些舊的props和state,且不是你想要的,你極可能遺漏了一些依賴。能夠嘗試使用這個lint 規則來訓練你發現這些依賴。可能沒過幾天,這種能力會變得像是你的次日性。一樣能夠看咱們官網FAQ中的這個回答。
我但願這個摘要對你有所幫助!要不,咱們開始正文。
在咱們討論effects以前,咱們須要先討論一下渲染(rendering)。
咱們來看一個計數器組件Counter:
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
會"監聽"狀態的變化並自動更新嗎?這麼想多是學習React的時候有用的第一直覺,但它並非精確的心智模型。
上面例子中, count
僅是一個數字而已。它不是神奇的"data binding", "watcher", "proxy",或者其餘任何東西。它就是一個普通的數字像下面這個同樣:
const count = 42; <p>You clicked {count} times </p>
咱們的組件第一次渲染的時候,從 useState()
拿到 count
的初始值 0
。當咱們調用 setCount(1)
,React會再次渲染組件,這一次 count
是 1
。如此等等:
function Counter() { const count = 0; <p>You clicked {count} times</p> } function Counter() { const count = 1; <p>You clicked {count} times</p> } function Counter() { const count = 2; <p>You clicked {count} times</p> }
當咱們更新狀態的時候,React會從新渲染組件。每一次渲染都能拿到獨立的 count
狀態,這個狀態值是函數中的一個常量。
因此下面的這行代碼沒有作任何特殊的數據綁定:
<p>You clicked {count} times</p>
它僅僅只是在渲染輸出中插入了count這個數字。這個數字由React提供。當 setCount
的時候,React會帶着一個不一樣的 count
值再次調用組件。而後,React會更新DOM以保持和渲染輸出一致。
這裏關鍵的點在於任意一次渲染中的 count
常量都不會隨着時間改變。渲染輸出會變是由於咱們的組件被一次次調用,而每一次調用引發的渲染中,它包含的 count
值獨立於其餘渲染。
(關於這個過程更深刻的探討能夠查看個人另外一篇文章React as a UI Runtime 。)
到目前爲止一切都還好。那麼事件處理函數呢?
看下面的這個例子。它在三秒後會alert點擊次數 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> ); }
若是我按照下面的步驟去操做:
來本身 試試吧!
這篇文章深刻探索了箇中原因。正確的答案就是 3 。alert會"捕獲"我點擊按鈕時候的狀態。
(雖然有其餘辦法能夠實現不一樣的行爲,但如今我會專一於這個默認的場景。當咱們在構建一種心智模型的時候,在可選的策略中分辨出"最小阻力路徑"是很是重要的。)
但它到底是如何工做的呢?
咱們發現 count
在每一次函數調用中都是一個常量值。值得強調的是 — 咱們的組件函數每次渲染都會被調用,可是每一次調用中 count
值都是常量,而且它被賦予了當前渲染中的狀態值。
這並非React特有的,普通的函數也有相似的行爲:
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中, _當前_的組件狀態會改變同樣)。 而後,在 sayHi
函數中,局部常量 name
會和某次調用中的 person
關聯。由於這個常量是局部的,因此每一次調用都是相互獨立的。結果就是,當定時器回調觸發的時候,每個alert都會彈出它擁有的 name
。
這就解釋了咱們的事件處理函數如何捕獲了點擊時候的 count
值。若是咱們應用相同的替換原理,每一次渲染"看到"的是它本身的 count
:
function Counter() { const count = 0; function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } } function Counter() { const count = 1; function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } } function Counter() { const count = 2; function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } }
因此實際上,每一次渲染都有一個"新版本"的 handleAlertClick
。每個版本的 handleAlertClick
"記住" 了它本身的 count
:
function Counter() { function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + 0); }, 3000); } <button onClick={handleAlertClick} /> } function Counter() { function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + 1); }, 3000); } <button onClick={handleAlertClick} /> } function Counter() { function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + 2); }, 3000); } <button onClick={handleAlertClick} /> }
這就是爲何在這個demo中中,事件處理函數"屬於"某一次特定的渲染,當你點擊的時候,它會使用那次渲染中 counter
的狀態值。
在任意一次渲染中,props和state是始終保持不變的。若是props和state在不一樣的渲染中是相互獨立的,那麼使用到它們的任何值也是獨立的(包括事件處理函數)。它們都"屬於"一次特定的渲染。即使是事件處理中的異步函數調用"看到"的也是此次渲染中的 count
值。
備註:上面我將具體的 count
值直接內聯到了 handleAlertClick
函數中。這種心智上的替換是安全的由於 count
值在某次特定渲染中不可能被改變。它被聲明成了一個常量而且是一個數字。這樣去思考其餘類型的值好比對象也一樣是安全的,固然須要在咱們都贊成應該避免直接修改state這個前提下。經過調用 setSomething(newObj)
的方式去生成一個新的對象而不是直接修改它是更好的選擇,由於這樣能保證以前渲染中的state不會被污染。
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> ); }
拋一個問題給你:effect是如何讀取到最新的 count
狀態值的呢?
也許,是某種"data binding"或"watching"機制使得 count
可以在effect函數內更新?也或許 count
是一個可變的值,React會在咱們組件內部修改它以使咱們的effect函數總能拿到最新的值?
都不是。
咱們已經知道 count
是某個特定渲染中的常量。事件處理函數"看到"的是屬於它那次特定渲染中的 count
狀態值。對於effects也一樣如此:
並非 count
的值在"不變"的effect中發生了改變,而是 effect 函數自己 在每一次渲染中都不相同。
每個effect版本"看到"的 count
值都來自於它屬於的那次渲染:
function Counter() { useEffect( () => { document.title = `You clicked ${0} times`; } ); } function Counter() { useEffect( () => { document.title = `You clicked ${1} times`; } ); } function Counter() { useEffect( () => { document.title = `You clicked ${2} times`; } ); }
React會記住你提供的effect函數,而且會在每次更改做用於DOM並讓瀏覽器繪製屏幕後去調用它。
因此雖然咱們說的是一個 effect(這裏指更新document的title),但其實每次渲染都是一個 不一樣的函數 — 而且每一個effect函數"看到"的props和state都來自於它屬於的那次特定渲染。
概念上,你能夠想象effects是渲染結果的一部分。
嚴格地說,它們並非(爲了容許Hook的組合而且不引入笨拙的語法或者運行時)。可是在咱們構建的心智模型上,effect函數 _屬於_某個特定的渲染,就像事件處理函數同樣。
爲了確保咱們已經有了紮實的理解,咱們再回顧一下第一次的渲染過程:
0
時候的UI。<span> You clicked 0 times</span>
。() => { document.title = 'You clicked 0 times' }
。() => { document.title = 'You clicked 0 times' }
。如今咱們回顧一下咱們點擊以後發生了什麼:
1
。1
時候的UI。<span> You clicked 1 times</span>
。() => { document.title = 'You clicked 1 times' }
。() => { document.title = 'You clicked 1 times' }
。咱們如今知道effects會在每次渲染後運行,而且概念上它是組件輸出的一部分,能夠"看到"屬於某次特定渲染的props和state。
咱們來作一個思想實驗,思考下面的代碼:
function Counter() { const [count, setCount] = useState(0); 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> ); }
若是我點擊了不少次而且在effect裏設置了延時,打印出來的結果會是什麼呢?
你可能會認爲這是一個很繞的題而且結果是反直覺的。徹底錯了!咱們看到的就是順序的打印輸出 — 每個都屬於某次特定的渲染,所以有它該有的 count
值。你能夠本身試一試:
不過,class中的 this.state
並非這樣運做的。你可能會想固然覺得下面的class 實現和上面是相等的:
this.state.count
老是指向 _最新_的count值,而不是屬於某次特定渲染的值。因此你會看到每次打印輸出都是 5
:
我以爲Hooks這麼依賴Javascript閉包是挺諷刺的一件事。有時候組件的class實現方式會受閉包相關的苦(the canonical wrong-value-in-a-timeout confusion),但其實這個例子中真正的混亂來源是可變數據(React 修改了class中的 this.state
使其指向最新狀態),並非閉包自己的錯。
當封閉的值始終不會變的狀況下閉包是很是棒的。這使它們很是容易思考由於你本質上在引用常量。正如咱們所討論的,props和state在某個特定渲染中是不會改變的。順便說一下,咱們能夠使用閉包修復上面的class版本...
到目前爲止,咱們能夠明確地喊出下面重要的事實: 每個組件內的函數(包括事件處理函數,effects,定時器或者API調用等等)會捕獲某次渲染中定義的props和state。
因此下面的兩個例子是相等的:
function Example(props) { useEffect(() => { setTimeout(() => { console.log(props.counter); }, 1000); }); }
function Example(props) { const counter = props.counter; useEffect(() => { setTimeout(() => { console.log(counter); }, 1000); }); }
在組件內何時去讀取props或者state是可有可無的。由於它們不會改變。在單次渲染的範圍內,props和state始終保持不變。(解構賦值的props使得這一點更明顯。)
固然,有時候你可能想在effect的回調函數裏讀取最新的值而不是捕獲的值。最簡單的實現方法是使用refs,這篇文章的最後一部分介紹了相關內容。
須要注意的是當你想要從 過去 渲染中的函數裏讀取 將來 的props和state,你是在逆潮而動。雖然它並無 錯(有時候可能也須要這樣作),但它由於打破了默認範式會使代碼顯得不夠"乾淨"。這是咱們有意爲之的,由於它能幫助突出哪些代碼是脆弱的,是須要依賴時間次序的。在class中,若是發生這種狀況就沒那麼顯而易見了。
下面這個計數器版本 模擬了class中的行爲:
function Example() { const [count, setCount] = useState(0); const latestCount = useRef(count); useEffect(() => { latestCount.current = count; setTimeout(() => { console.log(`You clicked ${latestCount.current} times`); }, 3000); }); }
在React中去直接修改值看上去有點怪異。然而,在class組件中React正是這樣去修改 this.state
的。不像捕獲的props和state,你無法保證在任意一個回調函數中讀取的 latestCount.current
是不變的。根據定義,你能夠隨時修改它。這就是爲何它不是默認行爲,而是須要你主動選擇這樣作。
像 文檔中解釋的, 有些 effects 可能須要有一個清理步驟。本質上,它的目的是消除反作用(effect),好比取消訂閱。
思考下面的代碼:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange); }; });
假設第一次渲染的時候 props
是 {id: 10}
,第二次渲染的時候是 {id: 20}
。
React只會在瀏覽器繪製後運行effects。這使得你的應用更流暢由於大多數effects並不會阻塞屏幕的更新。Effect的清除一樣被延遲了。 上一次的effect會在從新渲染後被清除:
{id: 20}
的UI。{id: 20}
的UI。{id: 10}
的effect。{id: 20}
的effect。你可能會好奇:若是清除上一次的effect發生在props變成 {id: 20}
以後,那它爲何還能"看到"舊的 {id: 10}
?
引用上半部分獲得的結論:
組件內的每個函數(包括事件處理函數,effects,定時器或者API調用等等)會捕獲定義它們的那次渲染中的props和state。
如今答案顯而易見。effect的清除並不會讀取"最新"的props。它只能讀取到定義它的那次渲染中的props值:
我最喜歡React的一點是它統一描述了初始渲染和以後的更新。這下降了你程序的熵。
好比我有個組件像下面這樣:
function Greeting({ name }) { return ( <h1 className="Greeting"> Hello, {name} </h1> ); }
我先渲染 <greeting name="Dan"></greeting>
而後渲染 <greeting name="Yuzhi"></greeting>
,和我直接渲染 <greeting name="Yuzhi"></greeting>
並無什麼區別。在這兩種狀況中,我最後看到的都是"Hello, Yuzhi"。
人們老是說:"重要的是旅行過程,而不是目的地"。在React世界中,剛好相反。 重要的是目的,而不是過程。這就是JQuery代碼中 $.addClass
或 $.removeClass
這樣的調用(過程)和React代碼中聲明CSS類名 應該是什麼(目的)之間的區別。
React會根據咱們當前的props和state同步到DOM。"mount"和"update"之於渲染並無什麼區別。
你應該以相同的方式去思考effects。 ** useEffect
使你可以根據props和state 同步 React tree以外的東西。**
function Greeting({ name }) { useEffect(() => { document.title = 'Hello, ' + name; }); return ( <h1 className="Greeting"> Hello, {name} </h1> ); }
這就是和你們熟知的 _mount/update/unmount_心智模型之間細微的區別。理解和內化這種區別是很是重要的。 若是你試圖寫一個effect會根據是否第一次渲染而表現不一致,你正在逆潮而動。若是咱們的結果依賴於過程而不是目的,咱們會在同步中犯錯。
先渲染屬性A,B再渲染C,和當即渲染C並無什麼區別。雖然他們可能短暫地會有點不一樣(好比請求數據時),但最終的結果是同樣的。
不過話說回來,在 _每一次_渲染後都去運行全部的effects可能並不高效。(而且在某些場景下,它可能會致使無限循環。)
因此咱們該怎麼解決這個問題?
其實咱們已經從React處理DOM的方式中學習到了解決辦法。React只會更新DOM真正發生改變的部分,而不是每次渲染都大動干戈。
當你把
<h1 className="Greeting"> Hello, Dan </h1>
更新到
<h1 className="Greeting"> Hello, Yuzhi </h1>
React 可以看到兩個對象:
const oldProps = {className: 'Greeting', children: 'Hello, Dan'}; const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};
它會檢測每個props,而且發現 children
發生改變須要更新DOM,但 className
並無。因此它只須要這樣作:
domNode.innerText = 'Hello, Yuzhi';
咱們也能夠用相似的方式處理effects嗎?若是可以在不須要的時候避免調用effect就太好了。
舉個例子,咱們的組件可能由於狀態變動而從新渲染:
function Greeting({ name }) { const [counter, setCounter] = useState(0); useEffect(() => { document.title = 'Hello, ' + name; }); return ( <h1 className="Greeting"> Hello, {name} <button onClick={() => setCounter(counter + 1)}>Increment</button> </h1> ); }
可是咱們的effect並無使用 counter
這個狀態。 咱們的effect只會同步 name
屬性給 document.title
,但 name
並無變。在每一次counter改變後從新給 document.title
賦值並非理想的作法。
好了,那React能夠...區分effects的不一樣嗎?
let oldEffect = () => { document.title = 'Hello, Dan'; }; let newEffect = () => { document.title = 'Hello, Dan'; };
並不能。React並不能猜想到函數作了什麼若是不先調用的話。(源碼中並無包含特殊的值,它僅僅是引用了 name
屬性。)
這是爲何你若是想要避免effects沒必要要的重複調用,你能夠提供給 useEffect
一個依賴數組參數(deps):
useEffect(() => { document.title = 'Hello, ' + name; }, [name]);
這比如你告訴React:"Hey,我知道你看不到這個函數裏的東西,但我能夠保證只使用了渲染中的 name
,別無其餘。"
若是當前渲染中的這些依賴項和上一次運行這個effect的時候值同樣,由於沒有什麼須要同步React會自動跳過此次effect:
const oldEffect = () => { document.title = 'Hello, Dan'; }; const oldDeps = ['Dan']; const newEffect = () => { document.title = 'Hello, Dan'; }; const newDeps = ['Dan'];
即便依賴數組中只有一個值在兩次渲染中不同,咱們也不能跳過effect的運行。要同步全部!
關於依賴項對React撒謊會有很差的結果。直覺上,這很好理解,但我曾看到幾乎全部依賴class心智模型使用 useEffect
的人都試圖違反這個規則。(我剛開始也這麼幹了!)
function SearchResults() { async function fetchData() { } useEffect(() => { fetchData(); }, []); }
(官網的Hooks FAQ 解釋了應該怎麼作。 咱們在下面 會從新回顧這個例子。)
"但我只是想在掛載的時候運行它!",你可能會說。如今只須要記住:若是你設置了依賴項, effect中用到的全部組件內的值都要包含在依賴中。這包括props,state,函數 — 組件內的任何東西。
有時候你是這樣作了,但可能會引發一個問題。好比,你可能會遇到無限請求的問題,或者socket被頻繁建立的問題。 解決問題的方法不是移除依賴項。咱們會很快了解具體的解決方案。
不過在咱們深刻解決方案以前,咱們先嚐試更好地理解問題。
若是依賴項包含了全部effect中使用到的值,React就能知道什麼時候須要運行它:
useEffect(() => { document.title = 'Hello, ' + name; }, [name]);
(依賴發生了變動,因此會從新運行effect。)
可是若是咱們將 []
設爲effect的依賴,新的effect函數不會運行:
useEffect(() => { document.title = 'Hello, ' + name; }, []);
(依賴沒有變,因此不會再次運行effect。)
在這個例子中,問題看起來顯而易見。但在某些狀況下若是你腦子裏"跳出"class組件的解決辦法,你的直覺極可能會欺騙你。
舉個例子,咱們來寫一個每秒遞增的計數器。在Class組件中,咱們的直覺是:"開啓一次定時器,清除也是一次"。這裏有一個例子說明怎麼實現它。當咱們理所固然地把它用 useEffect
的方式翻譯,直覺上咱們會設置依賴爲 []
。"我只想運行一次effect",對嗎?
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}h1>; }
然而,這個例子只會遞增一次。 天了嚕。
若是你的心智模型是"只有當我想從新觸發effect的時候才須要去設置依賴",這個例子可能會讓你產生存在危機。你想要觸發一次由於它是定時器 — 但爲何會有問題?
若是你知道依賴是咱們給React的暗示,告訴它effect全部須要使用的渲染中的值,你就不會吃驚了。effect中使用了 count
但咱們撒謊說它沒有依賴。若是咱們這樣作早晚會出幺蛾子。
在第一次渲染中, count
是 0
。所以, setCount(count + 1)
在第一次渲染中等價於 setCount(0 + 1)
。 既然咱們設置了 []
依賴,effect不會再從新運行,它後面每一秒都會調用 setCount(0 + 1)
:
function Counter() { useEffect( () => { const id = setInterval(() => { setCount(0 + 1); }, 1000); return () => clearInterval(id); }, [] ); } function Counter() { useEffect( () => { const id = setInterval(() => { setCount(1 + 1); }, 1000); return () => clearInterval(id); }, [] ); }
咱們對React撒謊說咱們的effect不依賴組件內的任何值,可實際上咱們的effect有依賴!
咱們的effect依賴 count
- 它是組件內的值(不過在effect外面定義):
const count = //... useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []);
所以,設置 []
爲依賴會引入一個bug。React會對比依賴,而且跳事後面的effect:
(依賴沒有變,因此不會再次運行effect。)
相似於這樣的問題是很難被想到的。所以,我鼓勵你將誠實地告知effect依賴做爲一條硬性規則,而且要列出因此依賴。(咱們提供了一個lint規則若是你想在你的團隊內作硬性規定。)
有兩種誠實告知依賴的策略。你應該從第一種開始,而後在須要的時候應用第二種。
第一種策略是在依賴中包含全部effect中用到的組件內的值。讓咱們在依賴中包含 count
:
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]);
如今依賴數組正確了。雖然它可能不是 _太理想_但確實解決了上面的問題。如今,每次 count
修改都會從新運行effect,而且定時器中的 setCount(count + 1)
會正確引用某次渲染中的 count
值:
function Counter() { useEffect( () => { const id = setInterval(() => { setCount(0 + 1); }, 1000); return () => clearInterval(id); }, [0] ); } function Counter() { useEffect( () => { const id = setInterval(() => { setCount(1 + 1); }, 1000); return () => clearInterval(id); }, [1] ); }
這能解決問題可是咱們的定時器會在每一次 count
改變後清除和從新設定。這應該不是咱們想要的結果:
(依賴發生了變動,因此會從新運行effect。)
第二種策略是修改effect內部的代碼以確保它包含的值只會在須要的時候發生變動。咱們不想告知錯誤的依賴 - 咱們只是修改effect使得依賴更少。
讓咱們來看一些移除依賴的經常使用技巧。
咱們想去掉effect的 count
依賴。
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]);
爲了實現這個目的,咱們須要問本身一個問題: 咱們爲何要用 count
?能夠看到咱們只在 setCount
調用中用到了 count
。在這個場景中,咱們其實並不須要在effect中使用 count
。當咱們想要根據前一個狀態更新狀態的時候,咱們可使用 setState
的函數形式:
useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []);
我喜歡把相似這種狀況稱爲"錯誤的依賴"。是的,由於咱們在effect中寫了 setCount(count + 1)
因此 count
是一個必需的依賴。可是,咱們真正想要的是把 count
轉換爲 count+1
,而後返回給React。但是React其實已經知道當前的 count
。 咱們須要告知React的僅僅是去遞增狀態 - 無論它如今具體是什麼值。
這正是 setCount(c => c + 1)
作的事情。你能夠認爲它是在給React"發送指令"告知如何更新狀態。這種"更新形式"在其餘狀況下也有幫助,好比你須要 批量更新。
注意咱們作到了移除依賴,而且沒有撒謊。咱們的effect再也不讀取渲染中的 count
值。
(依賴沒有變,因此不會再次運行effect。)
你能夠本身 試試。
儘管effect只運行了一次,第一次渲染中的定時器回調函數能夠完美地在每次觸發的時候給React發送 c => c + 1
更新指令。它再也不須要知道當前的 count
值。由於React已經知道了。
還記得咱們說過同步纔是理解effects的心智模型嗎?同步的一個有趣地方在於你一般想要把同步的"信息"和狀態解耦。舉個例子,當你在Google Docs編輯文檔的時候,Google並不會把整篇文章發送給服務器。那樣作會很是低效。相反的,它只是把你的修改以一種形式發送給服務端。
雖然咱們effect的狀況不盡相同,但能夠應用相似的思想。 只在effects中傳遞最小的信息會頗有幫助。 相似於 setCount(c => c + 1)
這樣的更新形式比 setCount(count + 1)
傳遞了更少的信息,由於它再也不被當前的count值"污染"。它只是表達了一種行爲("遞增")。"Thinking in React"也討論了如何找到最小狀態。原則是相似的,只不過如今關注的是如何更新。
表達 意圖(而不是結果)和Google Docs 如何處理共同編輯殊途同歸。雖然這個類比略微延伸了一點,函數式更新在React中扮演了相似的角色。它們確保能以批量地和可預測的方式來處理各類源頭(事件處理函數,effect中的訂閱,等等)的狀態更新。
然而,即便是 setCount(c => c + 1)
也並不完美。 它看起來有點怪,而且很是受限於它能作的事。舉個例子,若是咱們有兩個互相依賴的狀態,或者咱們想基於一個prop來計算下一次的state,它並不能作到。幸運的是, setCount(c => c + 1)
有一個更強大的姐妹模式,它的名字叫 useReducer
。
咱們來修改上面的例子讓它包含兩個狀態: count
和 step
。咱們的定時器會每次在count上增長一個 step
值:
function Counter() { const [count, setCount] = useState(0); const [step, setStep] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [step]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => setStep(Number(e.target.value))} /> </> ); }
(這裏是demo.)
注意 咱們沒有撒謊。既然咱們在effect裏使用了 step
,咱們就把它加到依賴裏。因此這也是爲何代碼能運行正確。
這個例子目前的行爲是修改 step
會重啓定時器 - 由於它是依賴項之一。在大多數場景下,這正是你所須要的。清除上一次的effect而後從新運行新的effect並無任何錯。除非咱們有很好的理由,咱們不該該改變這個默認行爲。
不過,假如咱們不想在 step
改變後重啓定時器,咱們該如何從effect中移除對 step
的依賴呢?
當你想更新一個狀態,而且這個狀態更新依賴於另外一個狀態的值時,你可能須要用 useReducer
去替換它們。
當你寫相似 setSomething(something => ...)
這種代碼的時候,也許就是考慮使用reducer的契機。reducer可讓你 把組件內發生了什麼(actions)和狀態如何響應並更新分開表述。
咱們用一個 dispatch
依賴去替換effect的 step
依賴:
const initialState = { count: 0, step: 1, }; function reducer(state, action) { const { count, step } = state; if (action.type === 'tick') { return { count: count + step, step }; } else if (action.type === 'step') { return { count, step: action.step }; } else { throw new Error(); } }
const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]);
(查看 demo。)
你可能會問:"這怎麼就更好了?"答案是 React會保證 dispatch
在組件的聲明週期內保持不變。因此上面例子中再也不須要從新訂閱定時器。
咱們解決了問題!
(你能夠從依賴中去除 dispatch
, setState
, 和 useRef
包裹的值由於React會確保它們是靜態的。不過你設置了它們做爲依賴也沒什麼問題。)
相比於直接在effect裏面讀取狀態,它dispatch了一個 _action_來描述發生了什麼。這使得咱們的effect和 step
狀態解耦。咱們的effect再也不關心怎麼更新狀態,它只負責告訴咱們發生了什麼。更新的邏輯全都交由reducer去統一處理:
(這裏是demo 若是你以前錯過了。)
咱們已經學習到如何移除effect的依賴,無論狀態更新是依賴上一個狀態仍是依賴另外一個狀態。 但假如咱們須要依賴 props 去計算下一個狀態呢?舉個例子,也許咱們的API是 <counter step="{1}"></counter>
。肯定的是,在這種狀況下,咱們無法避免依賴 props.step
。是嗎?
實際上, 咱們能夠避免!咱們能夠把 _reducer_函數放到組件內去讀取props:
function Counter({ step }) { const [count, dispatch] = useReducer(reducer, 0); function reducer(state, action) { if (action.type === 'tick') { return state + step; } else { throw new Error(); } } useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]); return <h1>{count}</h1>; }
這種模式會使一些優化失效,因此你應該避免濫用它,不過若是你須要你徹底能夠在reducer裏面訪問props。(這裏是demo。)
即便是在這個例子中,React也保證 dispatch
在每次渲染中都是同樣的。 因此你能夠在依賴中去掉它。它不會引發effect沒必要要的重複執行。
你可能會疑惑:這怎麼可能?在以前渲染中調用的reducer怎麼"知道"新的props?答案是當你 dispatch
的時候,React只是記住了action - 它會在下一次渲染中再次調用reducer。在那個時候,新的props就能夠被訪問到,並且reducer調用也不是在effect裏。
這就是爲何我傾向認爲 useReducer
是Hooks的"做弊模式"。它能夠把更新邏輯和描述發生了什麼分開。結果是,這能夠幫助我移除沒必要需的依賴,避免沒必要要的effect調用。
一個典型的誤解是認爲函數不該該成爲依賴。舉個例子,下面的代碼看上去能夠運行正常:
function SearchResults() { const [data, setData] = useState({ hits: [] }); async function fetchData() { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } useEffect(() => { fetchData(); }, []);
(這個例子 改編自Robin Wieruch這篇很棒的文章 —點擊查看 !)
須要明確的是,上面的代碼能夠正常工做。 但這樣作在組件日漸複雜的迭代過程當中咱們很難確保它在各類狀況下還能正常運行。
想象一下咱們的代碼作下面這樣的分離,而且每個函數的體量是如今的五倍,而後咱們在某些函數內使用了某些state或者prop:
function SearchResults() { const [query, setQuery] = useState('react'); function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } useEffect(() => { fetchData(); }, []); }
若是咱們忘記去更新使用這些函數(極可能經過其餘函數調用)的effects的依賴,咱們的effects就不會同步props和state帶來的變動。這固然不是咱們想要的。
幸運的是,對於這個問題有一個簡單的解決方案。 若是某些函數僅在effect中調用,你能夠把它們的定義移到effect中:
function SearchResults() { useEffect(() => { function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=react'; } async function fetchData() const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, []); }
(這裏是demo.)
這麼作有什麼好處呢?咱們再也不須要去考慮這些"間接依賴"。咱們的依賴數組也再也不撒謊: 在咱們的effect中確實沒有再使用組件範圍內的任何東西。
若是咱們後面修改 getFetchUrl
去使用 query
狀態,咱們更可能會意識到咱們正在effect裏面編輯它 - 所以,咱們須要把 query
添加到effect的依賴裏:
function SearchResults() { const [query, setQuery] = useState('react'); useEffect(() => { function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, [query]); }
(這裏是demo.)
添加這個依賴,咱們不只僅是在"取悅React"。在query改變後去從新請求數據是合理的。 useEffect
的設計意圖就是要強迫你關注數據流的改變,而後決定咱們的effects該如何和它同步 - 而不是忽視它直到咱們的用戶遇到了bug。
感謝 eslint-plugin-react-hooks
插件的 exhaustive-deps
lint規則,它會在你編碼的時候就分析effects而且提供可能遺漏依賴的建議。換句話說,機器會告訴你組件中哪些數據流變動沒有被正確地處理。
很是棒。
有時候你可能不想把函數移入effect裏。好比,組件內有幾個effect使用了相同的函數,你不想在每一個effect裏複製黏貼一遍這個邏輯。也或許這個函數是一個prop。
在這種狀況下你應該忽略對函數的依賴嗎?我不這麼認爲。再次強調, effects不該該對它的依賴撒謊。一般咱們還有更好的解決辦法。一個常見的誤解是,"函數歷來不會改變"。可是這篇文章你讀到如今,你知道這顯然不是事實。實際上,在組件內定義的函數每一次渲染都在變。
函數每次渲染都會改變這個事實自己就是個問題。 好比有兩個effects會調用 getFetchUrl
:
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); }, []); useEffect(() => { const url = getFetchUrl('redux'); }, []); }
在這個例子中,你可能不想把 getFetchUrl
移到effects中,由於你想複用邏輯。
另外一方面,若是你對依賴很"誠實",你可能會掉到陷阱裏。咱們的兩個effects都依賴 getFetchUrl
, 而它每次渲染都不一樣,因此咱們的依賴數組會變得無用:
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); }, [getFetchUrl]); }
一個可能的解決辦法是把 getFetchUrl
從依賴中去掉。可是,我不認爲這是好的解決方式。這會使咱們後面對數據流的改變很難被發現從而忘記去處理。這會致使相似於上面"定時器不更新值"的問題。
相反的,咱們有兩個更簡單的解決辦法。
第一個, 若是一個函數沒有使用組件內的任何值,你應該把它提到組件外面去定義,而後就能夠自由地在effects中使用:
function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } function SearchResults() { useEffect(() => { const url = getFetchUrl('react'); }, []); useEffect(() => { const url = getFetchUrl('redux'); }, []); }
你再也不須要把它設爲依賴,由於它們不在渲染範圍內,所以不會被數據流影響。它不可能忽然意外地依賴於props或state。
或者, 你也能夠把它包裝成 useCallback
Hook:
function SearchResults() { const getFetchUrl = useCallback((query) => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, []); useEffect(() => { const url = getFetchUrl('react'); }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); }, [getFetchUrl]); }
useCallback
本質上是添加了一層依賴檢查。它以另外一種方式解決了問題 - 咱們使函數自己只在須要的時候才改變,而不是去掉對函數的依賴。
咱們來看看爲何這種方式是有用的。以前,咱們的例子中展現了兩種搜索結果(查詢條件分別爲 'react'
和 'redux'
)。但若是咱們想添加一個輸入框容許你輸入任意的查詢條件(query
)。不一樣於傳遞 query
參數的方式,如今 getFetchUrl
會從狀態中讀取。
咱們很快發現它遺漏了 query
依賴:
function SearchResults() { const [query, setQuery] = useState('react'); const getFetchUrl = useCallback(() => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, []); }
若是我把 query
添加到 useCallback
的依賴中,任何調用了 getFetchUrl
的effect在 query
改變後都會從新運行:
function SearchResults() { const [query, setQuery] = useState('react'); const getFetchUrl = useCallback(() => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, [query]); useEffect(() => { const url = getFetchUrl(); }, [getFetchUrl]); }
咱們要感謝 useCallback
,由於若是 query
保持不變, getFetchUrl
也會保持不變,咱們的effect也不會從新運行。可是若是 query
修改了, getFetchUrl
也會隨之改變,所以會從新請求數據。這就像你在Excel裏修改了一個單元格的值,另外一個使用它的單元格會自動從新計算同樣。
這正是擁抱數據流和同步思惟的結果。 對於經過屬性從父組件傳入的函數這個方法也適用:
function Parent() { const [query, setQuery] = useState('react'); const fetchData = useCallback(() => { const url = 'https://hn.algolia.com/api/v1/search?query=' + query; }, [query]); return <Child fetchData={fetchData} /> } function Child({ fetchData }) { let [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, [fetchData]); }
由於 fetchData
只有在 Parent
的 query
狀態變動時纔會改變,因此咱們的 Child
只會在須要的時候纔去從新請求數據。
有趣的是,這種模式在class組件中行不通,而且這種行不通恰到好處地揭示了effect和生命週期範式之間的區別。考慮下面的轉換:
class Parent extends Component { state = { query: 'react' }; fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query; }; render() { return <Child fetchData={this.fetchData} />; } } class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } render() { } }
你可能會想:"少來了Dan,咱們都知道 useEffect
就像 componentDidMount
和 componentDidUpdate
的結合,你不能總是破壞這一條!" 好吧,就算加了 componentDidUpdate
照樣無用:
class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } componentDidUpdate(prevProps) { if (this.props.fetchData !== prevProps.fetchData) { this.props.fetchData(); } } render() { } }
固然如此, fetchData
是一個class方法!(或者你也能夠說是class屬性 - 但這不能改變什麼。)它不會由於狀態的改變而不一樣,因此 this.props.fetchData
和 prevProps.fetchData
始終相等,所以不會從新請求。那咱們刪掉條件判斷怎麼樣?
componentDidUpdate(prevProps) { this.props.fetchData(); }
等等,這樣會在每次渲染後都去請求。(添加一個加載動畫多是一種有趣的發現這種狀況的方式。)也許咱們能夠綁定一個特定的query?
render() { return <Child fetchData={this.fetchData.bind(this, this.state.query)} />; }
但這樣一來, this.props.fetchData !== prevProps.fetchData
表達式永遠是 true
,即便 query
並未改變。這會致使咱們老是去請求。
想要解決這個class組件中的難題,惟一現實可行的辦法是硬着頭皮把 query
自己傳入 Child
組件。 Child
雖然實際並無直接 _使用_這個 query
的值,但能在它改變的時候觸發一次從新請求:
class Parent extends Component { state = { query: 'react' }; fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query; }; render() { return <Child fetchData={this.fetchData} query={this.state.query} />; } } class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { this.props.fetchData(); } } render() { } }
在使用React的class組件這麼多年後,我已經如此習慣於把沒必要要的props傳遞下去而且破壞父組件的封裝以致於我在一週以前才意識到我爲何必定要這樣作。
在class組件中,函數屬性自己並非數據流的一部分。組件的方法中包含了可變的 this
變量致使咱們不能肯定無疑地認爲它是不變的。所以,即便咱們只須要一個函數,咱們也必須把一堆數據傳遞下去僅僅是爲了作"diff"。咱們沒法知道傳入的 this.props.fetchData
是否依賴狀態,而且不知道它依賴的狀態是否改變了。
使用 useCallback
,函數徹底能夠參與到數據流中。咱們能夠說若是一個函數的輸入改變了,這個函數就改變了。若是沒有,函數也不會改變。感謝周到的 useCallback
,屬性好比 props.fetchData
的改變也會自動傳遞下去。
相似的,useMemo
可讓咱們對複雜對象作相似的事情。
function ColorPicker() { const [color, setColor] = useState('pink'); const style = useMemo(() => ({ color }), [color]); return <Child style={style} />; }
我想強調的是,處處使用 useCallback
是件挺笨拙的事。當咱們須要將函數傳遞下去而且函數會在子組件的effect中被調用的時候, useCallback
是很好的技巧且很是有用。或者你想試圖減小對子組件的記憶負擔,也不妨一試。但總的來講Hooks自己能更好地避免傳遞迴調函數。
在上面的例子中,我更傾向於把 fetchData
放在個人effect裏(它能夠抽離成一個自定義Hook)或者是從頂層引入。我想讓effects保持簡單,而在裏面調用回調會讓事情變得複雜。("若是某個 props.onComplete
回調改變了而請求還在進行中會怎麼樣?")你能夠模擬class的行爲但那樣並不能解決競態的問題。
下面是一個典型的在class組件裏發請求的例子:
class Article extends Component { state = { article: null }; componentDidMount() { this.fetchData(this.props.id); } async fetchData(id) { const article = await API.fetchArticle(id); this.setState({ article }); } }
你極可能已經知道,上面的代碼埋伏了一些問題。它並無處理更新的狀況。因此第二個你可以在網上找到的經典例子是下面這樣的:
class Article extends Component { state = { article: null }; componentDidMount() { this.fetchData(this.props.id); } componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.fetchData(this.props.id); } } async fetchData(id) { const article = await API.fetchArticle(id); this.setState({ article }); } }
這顯然好多了!但依舊有問題。有問題的緣由是請求結果返回的順序不能保證一致。好比我先請求 {id: 10}
,而後更新到 {id: 20}
,但 {id: 20}
的請求更先返回。請求更早但返回更晚的狀況會錯誤地覆蓋狀態值。
這被叫作競態,這在混合了 async
/ await
(假設在等待結果返回)和自頂向下數據流的代碼中很是典型(props和state可能會在async函數調用過程當中發生改變)。
Effects並無神奇地解決這個問題,儘管它會警告你若是你直接傳了一個 async
函數給effect。(咱們會改善這個警告來更好地解釋你可能會遇到的這些問題。)
若是你使用的異步方式支持取消,那太棒了。你能夠直接在清除函數中取消異步請求。
或者,最簡單的權宜之計是用一個布爾值來跟蹤它:
function Article({ id }) { const [article, setArticle] = useState(null); useEffect(() => { let didCancel = false; async function fetchData() { const article = await API.fetchArticle(id); if (!didCancel) { setArticle(article); } } fetchData(); return () => { didCancel = true; }; }, [id]); }
這篇文章討論了更多關於如何處理錯誤和加載狀態,以及抽離邏輯到自定義的Hook。我推薦你認真閱讀一下若是你想學習更多關於如何在Hooks裏請求數據的內容。
在class組件生命週期的思惟模型中,反作用的行爲和渲染輸出是不一樣的。UI渲染是被props和state驅動的,而且能確保步調一致,但反作用並非這樣。這是一類常見問題的來源。
而在 useEffect
的思惟模型中,默認都是同步的。反作用變成了React數據流的一部分。對於每個 useEffect
調用,一旦你處理正確,你的組件可以更好地處理邊緣狀況。
然而,用好 useEffect
的前期學習成本更高。這可能讓人氣惱。用同步的代碼去處理邊緣狀況自然就比觸發一次不用和渲染結果步調一致的反作用更難。
這不免讓人擔心若是 useEffect
是你如今使用最多的工具。不過,目前大抵還處理低水平使用階段。由於Hooks太新了因此你們都還在低水平地使用它,尤爲是在一些教程示例中。但在實踐中,社區極可能即將開始高水平地使用Hooks,由於好的API會有更好的動量和衝勁。
我看到不一樣的應用在創造他們本身的Hooks,好比封裝了應用鑑權邏輯的 useFetch
或者使用theme context的 useTheme
。你一旦有了包含這些的工具箱,你就不會那麼頻繁地直接使用 useEffect
。但每個基於它的Hook都能從它的適應能力中獲得益處。
目前爲止, useEffect
主要用於數據請求。可是數據請求準確說並非一個同步問題。由於咱們的依賴常常是 []
因此這一點尤爲明顯。那咱們究竟在同步什麼?
長遠來看, Suspense用於數據請求 會容許第三方庫經過第一等的途徑告訴React暫停渲染直到某些異步事物(任何東西:代碼,數據,圖片)已經準備就緒。
當Suspense逐漸地覆蓋到更多的數據請求使用場景,我預料 useEffect
會退居幕後做爲一個強大的工具,用於同步props和state到某些反作用。不像數據請求,它能夠很好地處理這些場景由於它就是爲此而設計的。不過在那以前,自定義的Hooks好比這兒提到的是複用數據請求邏輯很好的方式。
如今你差很少知道了我關於如何使用effects的全部知識,能夠檢查一下開頭的TLDR。你如今以爲它說得有道理嗎?我有遺漏什麼嗎?(個人紙尚未寫完!)
譯者寫了一個 React + Hooks 的 UI 庫,方便你們學習和使用,
歡迎關注公衆號「前端進階課」認真學前端,一塊兒進階。