前言: 每個人都有屬於本身的一片森林,也許咱們未曾去過,可是它一直在那裏,總會在那裏。迷失的人迷失了,相逢的人會再次相逢。 - 《挪威的森林》html
簡單來講Hooks規則就是咱們在使用Hooks編寫程序的時候須要遵循的規範。react
不要在循環,條件或者嵌套函數中調用Hook.git
不要在普通的 JavaScript 函數中調用 Hook.github
咱們接下來將會舉一個錯誤的例子,而且將會展開分析爲何不能這麼寫, 這麼寫會致使什麼錯誤發生。數組
⚠️ 錯誤示例(非完整版):架構
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
複製代碼
咱們都知道在組件中使用state hooks和effect hooks,靠的是Hook的調用順序,這樣React才能知道哪一個state對應那個useState。那麼咱們先來捋一下上述示例代碼Hooks的調用順序。dom
// ------------
// 首次渲染
// ------------
useState([0, 1, 2]) // 使用[0, 1, 2]數組初始化arr
useState('Hello World!') // 使用'Hello World!'初始化title
useState('Hello World!') // 使用'Hello World!'初始化title
useState('Hello World!') // 使用'Hello World!'初始化title
// ------------
// 第二次渲染
// ------------
useState([0, 1, 2]) // 讀取變量名爲arr的state
useState('Hello World!') // 讀取變量名爲title的state - (A hook)
useState('Hello World!') // 讀取變量名爲title的state - (B hook)
useState('Hello World!') // 讀取變量名爲title的state - (C hook)
複製代碼
以上就是Hooks的調用順序了,上述這段代碼確實沒有什麼問題,也能夠正常執行。接下來咱們稍微修改一下代碼。ide
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
+ useEffect(() => {
+ setTimeout(() => {
+ setArr([0, 1])
+ }, 500);
+ }, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
複製代碼
咱們拋開effect的鉤子不談,就看state的鉤子。咱們能夠很容易地得出第三次Hooks調用的順序是:函數
// ------------
// 第三次渲染
// ------------
useState([0, 1]) // 讀取變量名爲arr的state
useState('Hello World!') // 讀取變量名爲title的state
useState('Hello World!') // 讀取變量名爲title的state
複製代碼
咱們發現程序拋出異常了,緣由是: 從新渲染後的鉤子比預期的鉤子要少。spa
Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
咱們再🤔思考一下, 若是咱們第三次渲染的時候, 渲染的鉤子數量大於等於上一次的時候會不會拋出異常呢?咱們來試驗一下。
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
+ useEffect(() => {
+ setTimeout(() => {
+ setArr([0, 1, 2, 3])
+ }, 500);
+ }, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
複製代碼
咱們驚訝地發現,程序居然能夠正常運行。那麼這時候,咱們仔細推敲一下Hook的第一個規則: 不要在循環,條件或者嵌套函數中調用Hooks
, 其實這個規則的深層意思就是, 要讓上一次的Hook的知道它應該返回什麼。
什麼意思呢? 就是說咱們在第三次渲染的時候, 應該讓 A, B, C hook知道我應該返回什麼值。當arr變化爲[0, 1]的時候, C hook是不知道應該返回什麼東西的, 所以程序就會報錯。可是, 當arr變化爲[0, 1, 2, 3]的時候, A, B, C hook都知道本身應該返回什麼值, 所以程序能夠正常運行。既然本文講的是深度理解Hook規則,那麼咱們接下來將會進行源碼架構的分析。
爲了保證你們都能看懂,下面的內容不會過多地涉及Hooks源碼解析。
首先咱們得明白, Hook的更新流程是經過鏈表完成的。若是你們對於爲何用鏈表感興趣的能夠去看這篇文章: 無心識設計-覆盤React Hook的創造過程。 那麼鏈表的結構應該是怎麼樣的呢?
咱們來模擬一下上述例子首次渲染的過程:
初始化的時候(組件還未渲染): firstWorkInProgressHook = workInProgressHook = null
組件初次渲染的時候
firstWorkInProgressHook = workInProgressHook = hook1
workInProgressHook = workInProgressHook.next = hook2
workInProgressHook = workInProgressHook.next = hook3
workInProgressHook = workInProgressHook.next = hook4
這個過程,其實就是一個用鏈表存儲的過程, 那麼每個hook至少應該可以保存當前它本身的信息和下一個節點(hook)的信息而且擁有可以更新這個鏈表的功能。
type Hook = {
memoizedState: any, // 上次更新完的最終狀態
queue: UpdateQueue<any, any> | null, // 更新隊列
next: Hook | null, // 下一個hook
};
複製代碼
那麼咱們能夠很容易的摸出, 整個鏈表應該長什麼樣子:
const fiber = {
//...
memoizedState: {
memoizedState: [0, 1, 2],
queue: {
// ...
},
next: {
memoizedState: 'Hello World!',
queue: {
// ...
},
next: 'Hello World'
}
},
// ...
memoizedState: {
memoizedState: 'Hello World',
queue: {
// ...
},
next: {
memoizedState: 'Hello World!',
queue: {
// ...
},
next: 'Hello World'
}
},
//...
}
複製代碼
整個鏈表是在mount時構造的,所以當咱們執行update操做的時候必定要保證執行順序, 否則的話整個鏈表就亂了。這時候, 咱們聯想到Hook的第一條規則: 不要在循環,條件 或者嵌套函數中調用Hook, 你們應該可以大體理解爲何要遵照這個規則了吧。接下來, 咱們覆盤一下, 上面的錯誤例子,加深一下咱們的印象。
當咱們執行更新arr操做的時候, setArr([0, 1])
, 第三個hook的next會找不到下一個節點.所以會在finishHooks的時候會拋出異常。咱們能夠在react-dom.development.js
看到 當咱們更新到第三個hooks、的時候, 會出現找不到下一個hook的狀況, 所以didRenderTooFewHooks
爲 false
。因此拋出了上面例子中的異常。
// 源碼部分
function finishHooks() {
// ...
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
// ...
!!didRenderTooFewHooks ? invariant(false, 'Rendered fewer hooks than expected. This may be caused by an accidental early return statement.') : void 0;
// ...
}
複製代碼
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
+ return arr.map((_, index) => {
- return arr.map(() => {
- return Child();
+ return <Child key={index} />
});
};
useEffect(() => {
setTimeout(() => {
setArr([0, 1])
}, 500);
}, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
複製代碼