原文地址
React Hooks實(shí)例教學(xué)
目前,Hooks 應(yīng)該是 React 中最火的概念了拿穴,在閱讀這篇文章之前乌妙,希望你已經(jīng)了解了基本的 Hooks 用法。
在使用 Hooks 的時(shí)候靶端,我們可能會(huì)有很多疑惑
- 為什么只能在函數(shù)最外層調(diào)用 Hook谎势,不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用杨名?
- 為什么 useEffect 第二個(gè)參數(shù)是空數(shù)組脏榆,就相當(dāng)于 ComponentDidMount ,只會(huì)執(zhí)行一次台谍?
- 自定義的 Hook 是如何影響使用它的函數(shù)組件的须喂?
- Capture Value 特性是如何產(chǎn)生的?
- ......
這篇文章我們不會(huì)講解 Hooks 的概念和用法趁蕊,而是會(huì)帶你從零實(shí)現(xiàn)一個(gè) tiny hooks坞生,知其然知其所以然。
useState
-
最簡(jiǎn)單的 useState 用法是這樣的:
demo1: https://codesandbox.io/s/v0nqm309q3function Counter() { var [count, setCount] = useState(0); return ( <div> <div>{count}</div> <Button onClick={() => { setCount(count + 1); }}> 點(diǎn)擊 </Button> </div> ); }
-
基于 useState 的用法掷伙,我們嘗試著自己實(shí)現(xiàn)一個(gè) useState:
demo2:https://codesandbox.io/s/myy5qvoxppfunction useState(initialValue) { var state = initialValue; function setState(newState) { state = newState; render(); } return [state, setState]; }
-
這時(shí)我們發(fā)現(xiàn)是己,點(diǎn)擊 Button 的時(shí)候,count 并不會(huì)變化任柜,為什么呢赃泡?我們沒(méi)有存儲(chǔ) state寒波,每次渲染 Counter 組件的時(shí)候,state 都是新重置的升熊。
自然我們就能想到俄烁,把 state 提取出來(lái),存在 useState 外面级野。
demo3:https://codesandbox.io/s/q9wq6w5k3wvar _state; // 把 state 存儲(chǔ)在外面 function useState(initialValue) { _state = _state | initialValue; // 如果沒(méi)有 _state页屠,說(shuō)明是第一次執(zhí)行,把 initialValue 復(fù)制給它 function setState(newState) { _state = newState; render(); } return [_state, setState]; }
到目前為止蓖柔,我們實(shí)現(xiàn)了一個(gè)可以工作的 useState辰企,至少現(xiàn)在來(lái)看沒(méi)啥問(wèn)題。
接下來(lái)况鸣,讓我們看看 useEffect 是怎么實(shí)現(xiàn)的牢贸。
useEffect
useEffect 是另外一個(gè)基礎(chǔ)的 Hook,用來(lái)處理副作用镐捧,最簡(jiǎn)單的用法是這樣的:
demo4:https://codesandbox.io/s/93jp55qyp4
useEffect(() => {
console.log(count);
}, [count]);
我們知道 useEffect 有幾個(gè)特點(diǎn):
- 有兩個(gè)參數(shù) callback 和 dependencies 數(shù)組
- 如果 dependencies 不存在潜索,那么 callback 每次 render 都會(huì)執(zhí)行
- 如果 dependencies 存在,只有當(dāng)它發(fā)生了變化懂酱, callback 才會(huì)執(zhí)行
我們來(lái)實(shí)現(xiàn)一個(gè) useEffect
demo5:https://codesandbox.io/s/3kv3zlvzl1
let _deps; // _deps 記錄 useEffect 上一次的 依賴
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 兩次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在竹习,或者 dependencies 有變化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
到這里,我們又實(shí)現(xiàn)了一個(gè)可以工作的 useEffect列牺,似乎沒(méi)有那么難整陌。
此時(shí)我們應(yīng)該可以解答一個(gè)問(wèn)題:
Q:為什么第二個(gè)參數(shù)是空數(shù)組,相當(dāng)于 componentDidMount
瞎领?
A:因?yàn)橐蕾囈恢辈蛔兓诒瑁琧allback 不會(huì)二次執(zhí)行。
Not Magic, just Arrays
到現(xiàn)在為止九默,我們已經(jīng)實(shí)現(xiàn)了可以工作的 useState 和 useEffect震放。但是有一個(gè)很大的問(wèn)題:它倆都只能使用一次,因?yàn)橹挥幸粋€(gè) _state 和 一個(gè) _deps荤西。比如
const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
count 和 username 永遠(yuǎn)是相等的,因?yàn)樗麄児灿昧艘粋€(gè) _state伍俘,并沒(méi)有地方能分別存儲(chǔ)兩個(gè)值邪锌。我們需要可以存儲(chǔ)多個(gè) _state 和 _deps。
如 《React hooks: not magic, just arrays》所寫(xiě)癌瘾,我們可以使用數(shù)組觅丰,來(lái)解決 Hooks 的復(fù)用問(wèn)題。
demo6:https://codesandbox.io/s/50ww35vkzl
代碼關(guān)鍵在于:
- 初次渲染的時(shí)候妨退,按照 useState妇萄,useEffect 的順序蜕企,把 state,deps 等按順序塞到 memoizedState 數(shù)組中冠句。
- 更新的時(shí)候轻掩,按照順序,從 memoizedState 中把上次記錄的值拿出來(lái)懦底。
- 如果還是不清楚唇牧,可以看下面的圖。
let memoizedState = []; // hooks 存放在這個(gè)數(shù)組
let cursor = 0; // 當(dāng)前 memoizedState 下標(biāo)
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回當(dāng)前 state聚唐,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
我們用圖來(lái)描述 memoizedState 及 cursor 變化的過(guò)程丐重。
1. 初始化
2. 初次渲染
3. 事件觸發(fā)
4. Re Render
到這里,我們實(shí)現(xiàn)了一個(gè)可以任意復(fù)用的 useState 和 useEffect杆查。
同時(shí)扮惦,也可以解答幾個(gè)問(wèn)題:
Q:為什么只能在函數(shù)最外層調(diào)用 Hook?為什么不要在循環(huán)亲桦、條件判斷或者子函數(shù)中調(diào)用崖蜜。
A:memoizedState 數(shù)組是按 hook定義的順序來(lái)放置數(shù)據(jù)的,如果 hook 順序變化烙肺,memoizedState 并不會(huì)感知到纳猪。
Q:自定義的 Hook 是如何影響使用它的函數(shù)組件的?
A:共享同一個(gè) memoizedState桃笙,共享同一個(gè)順序氏堤。
Q:“Capture Value” 特性是如何產(chǎn)生的?
A:每一次 ReRender 的時(shí)候搏明,都是重新去執(zhí)行函數(shù)組件了鼠锈,對(duì)于之前已經(jīng)執(zhí)行過(guò)的函數(shù)組件,并不會(huì)做任何操作星著。
真正的 React 實(shí)現(xiàn)
雖然我們用數(shù)組基本實(shí)現(xiàn)了一個(gè)可用的 Hooks购笆,了解了 Hooks 的原理,但在 React 中虚循,實(shí)現(xiàn)方式卻有一些差異的同欠。
-
React 中是通過(guò)類似單鏈表的形式來(lái)代替數(shù)組的。通過(guò) next 按順序串聯(lián)所有的 hook横缔。
type Hooks = { memoizedState: any, // 指向當(dāng)前渲染節(jié)點(diǎn) Fiber baseState: any, // 初始化 initialState铺遂, 已經(jīng)每次 dispatch 之后 newState baseUpdate: Update<any> | null,// 當(dāng)前需要更新的 Update ,每次更新完之后茎刚,會(huì)賦值上一個(gè) update襟锐,方便 react 在渲染錯(cuò)誤的邊緣,數(shù)據(jù)回溯 queue: UpdateQueue<any> | null,// UpdateQueue 通過(guò) next: Hook | null, // link 到下一個(gè) hooks膛锭,通過(guò) next 串聯(lián)每一 hooks } type Effect = { tag: HookEffectTag, // effectTag 標(biāo)記當(dāng)前 hook 作用在 life-cycles 的哪一個(gè)階段 create: () => mixed, // 初始化 callback destroy: (() => mixed) | null, // 卸載 callback deps: Array<mixed> | null, next: Effect, // 同上 };
-
memoizedState粮坞,cursor 是存在哪里的蚊荣?如何和每個(gè)函數(shù)組件一一對(duì)應(yīng)的?
我們知道莫杈,react 會(huì)生成一棵組件樹(shù)(或Fiber 單鏈表)互例,樹(shù)中每個(gè)節(jié)點(diǎn)對(duì)應(yīng)了一個(gè)組件,hooks 的數(shù)據(jù)就作為組件的一個(gè)信息姓迅,存儲(chǔ)在這些節(jié)點(diǎn)上敲霍,伴隨組件一起出生,一起死亡丁存。