突破 Hooks 所有限制,只要 50 行代碼
大家好,我是卡頌。
你是否很討厭Hooks
調用順序的限制(Hooks
不能寫在條件語句裏)?
你是否遇到過在useEffect
中使用了某個state
,又忘記將其加入依賴項
,導致useEffect
回調執行時機出問題?
怪自己粗心?怪自己不好好看文檔?
答應我,不要怪自己。
根本原因在於React
沒有將Hooks
實現爲響應式更新。
是實現難度很高麼?本文會用 50 行代碼實現無限制版Hooks
,其中涉及的知識也是Vue
、Mobx
等基於響應式更新的庫的底層原理。
本文的正確食用方式是收藏後用電腦看,跟着我一起敲代碼(完整在線Demo
鏈接見閱讀原文)。
手機黨要是看了懵逼的話不要自責,是你食用方式不對。
注:本文代碼來自 Ryan Carniato 的文章 Building a Reactive Library from Scratch[1],老哥是
SolidJS
作者
萬丈高樓平地起
首先來實現useState
:
function useState(value) {
const getter = () => value;
const setter = (newValue) => value = newValue;
return [getter, setter];
}
返回值數組第一項負責取值,第二項負責賦值。相比React
,我們有個小改動:返回值的第一個參數是個函數而不是state
本身。
使用方式如下:
const [count, setCount] = useState(0);
console.log(count()); // 0
setCount(1);
console.log(count()); // 1
沒有黑魔法
接下來實現useEffect
,包括幾個要點:
-
依賴的
state
改變,useEffect
回調執行 -
不需要顯式的指定依賴項(即
React
中useEffect
的第二個參數)
舉個例子:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
useEffect(() => {
console.log('沒我啥事兒')
})
count
變化後第一個useEffect
會執行回調(因爲他內部依賴count
),但是第二個useEffect
不會執行。
前端沒有黑魔法,這裏是如何實現的呢?
答案是:訂閱發佈。
繼續用上面的例子來解釋訂閱發佈關係建立的時機:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
當useEffect
定義後他的回調會立刻執行一次,在其內部會執行:
window.title = count();
count
執行時會建立effect
與state
之間訂閱發佈的關係。
當下次執行setCount
(setter)時會通知訂閱了count
變化的useEffect
,執行其回調函數。
數據結構之間的關係如圖:
每個useState
內部有個集合subs
,用來保存**「訂閱該 state 變化」**的effect
。
effect
是每個useEffect
對應的數據結構:
const effect = {
execute,
deps: new Set()
}
其中:
-
execute
:該useEffect
的回調函數 -
deps
:該useEffect
依賴的state
對應subs
的集合
我知道你有點暈。看看上面的結構圖,緩緩,咱再繼續。
實現 useEffect
首先需要一個棧來保存當前正在執行的effect
。這樣當調用getter
時state
才知道應該與哪個effect
建立聯繫。
舉個例子:
// effect1
useEffect(() => {
window.title = count();
})
// effect2
useEffect(() => {
console.log('沒我啥事兒')
})
count
執行時需要知道自己處在effect1
的上下文中(而不是effect2
),這樣才能與effect1
建立聯繫。
// 當前正在執行effect的棧
const effectStack = [];
接下來實現useEffect
,包括如下功能點:
-
每次
useEffect
回調執行前重置依賴(回調內部state
的getter
會重建依賴關係) -
回調執行時確保當前
effect
處在effectStack
棧頂 -
回調執行後將當前
effect
從棧頂彈出
代碼如下:
function useEffect(callback) {
const execute = () => {
// 重置依賴
cleanup(effect);
// 推入棧頂
effectStack.push(effect);
try {
callback();
} finally {
// 出棧
effectStack.pop();
}
}
const effect = {
execute,
deps: new Set()
}
// 立刻執行一次,建立依賴關係
execute();
}
cleanup
用來移除該effect
與所有他依賴的state
之間的聯繫,包括:
-
訂閱關係:將該
effect
訂閱的所有state
變化移除 -
依賴關係:將該
effect
依賴的所有state
移除
function cleanup(effect) {
// 將該effect訂閱的所有state變化移除
for (const dep of effect.deps) {
dep.delete(effect);
}
// 將該effect依賴的所有state移除
effect.deps.clear();
}
移除後,執行useEffect
回調會再逐一重建關係。
改造 useState
接下來改造useState
,完成建立訂閱發佈關係的邏輯,要點如下:
-
調用
getter
時獲取當前上下文的effect
,建立關係 -
調用
setter
時通知所有訂閱該state
變化的effect
回調執行
function useState(value) {
// 訂閱列表
const subs = new Set();
const getter = () => {
// 獲取當前上下文的effect
const effect = effectStack[effectStack.length - 1];
if (effect) {
// 建立聯繫
subscribe(effect, subs);
}
return value;
}
const setter = (nextValue) => {
value = nextValue;
// 通知所有訂閱該state變化的effect回調執行
for (const sub of [...subs]) {
sub.execute();
}
}
return [getter, setter];
}
subscribe
的實現,同樣包括 2 個關係的建立:
function subscribe(effect, subs) {
// 訂閱關係建立
subs.add(effect);
// 依賴關係建立
effect.deps.add(subs);
}
讓我們來試驗下:
const [name1, setName1] = useState('KaSong');
useEffect(() => console.log('誰在那兒!', name1()))
// 打印: 誰在那兒!KaSong
setName1('KaKaSong');
// 打印: 誰在那兒!KaKaSong
實現 useMemo
接下來基於已有的 2 個hook
實現useMemo
:
function useMemo(callback) {
const [s, set] = useState();
useEffect(() => set(callback()));
return s;
}
自動依賴跟蹤
這套 50 行的Hooks
還有個強大的隱藏特性:自動依賴跟蹤。
我們拓展下上面的例子:
const [name1, setName1] = useState('KaSong');
const [name2, setName2] = useState('XiaoMing');
const [showAll, triggerShowAll] = useState(true);
const whoIsHere = useMemo(() => {
if (!showAll()) {
return name1();
}
return `${name1()} 和 ${name2()}`;
})
useEffect(() => console.log('誰在那兒!', whoIsHere()))
現在我們有 3 個state
:name1
、name2
、showAll
。
whoIsHere
作爲memo
,依賴以上三個state
。
最後,當whoIsHere
變化時,會觸發useEffect
回調。
當以上代碼運行後,基於初始的 3 個state
,會計算出whoIsHere
,進而觸發useEffect
回調,打印:
// 打印:誰在那兒!KaSong 和 XiaoMing
接下來調用:
setName1('KaKaSong');
// 打印:誰在那兒!KaKaSong 和 XiaoMing
triggerShowAll(false);
// 打印:誰在那兒!KaKaSong
下面的事情就有趣了,當調用:
setName2('XiaoHong');
並沒有log
打印。
這是因爲當triggerShowAll(false)
導致showAll state
爲false
後,whoIsHere
進入如下邏輯:
if (!showAll()) {
return name1();
}
由於沒有執行name2
,所以name2
與whoIsHere
已經沒有訂閱發佈關係了!
只有當triggerShowAll(true)
後,whoIsHere
進入如下邏輯:
return `${name1()} 和 ${name2()}`;
此時whoIsHere
纔會重新依賴name1
與name2
。
自動的依賴跟蹤,是不是很酷~
總結
至此,基於**「訂閱發佈」**,我們實現了可以**「自動依賴跟蹤」**的無限制Hooks
。
這套理念是最近幾年纔有人使用麼?
早在 2010 年初KnockoutJS
就用這種細粒度的方式實現響應式更新了。
不知道那時候,「Steve Sanderson」(KnockoutJS
作者)有沒有預見到 10 年後的今天,細粒度更新會在各種庫和框架中被廣泛使用。
參考資料
[1]
Building a Reactive Library from Scratch: https://dev.to/ryansolid/building-a-reactive-library-from-scratch-1i0p
我是卡頌,《React 技術揭祕》作者,全球開發者資訊觀察者
加我個人微信,我會:
-
每天在朋友圈分享
全球最新開發者資訊
-
拉你進
React源碼級進階羣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ghpJf2-qxD4nXjek5dqLHA