React Context 的核心實現,就 5 行代碼
大家好,我卡頌。
很多項目的源碼非常複雜,讓人望而卻步。但在打退堂鼓前,我們應該思考一個問題:源碼爲什麼複雜?
造成源碼複雜的原因不外乎有三個:
-
功能本身複雜,造成代碼複雜
-
編寫者功力不行,寫的代碼複雜
-
功能本身不復雜,但同一個模塊耦合了太多功能,看起來複雜
如果是原因 3,那實際理解起來其實並不難。我們需要的只是有人能幫我們剔除無關功能的干擾。
React Context
的實現就是個典型例子,當剔除無關功能的干擾後,他的核心實現,僅需 「5 行代碼」。
本文就讓我們看看React Context
的核心實現。
簡化模型
Context
的完整工作流程包括 3 步:
-
定義
context
-
賦值
context
-
消費
context
以下面的代碼舉例:
const ctx = createContext(null);
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
function Cpn() {
const num = useContext(ctx);
return <div>{num}</div>;
}
其中:
-
const ctx = createContext(null)
用於定義 -
<ctx.Provider value={1}>
用於賦值 -
const num = useContext(ctx)
用於消費
Context
數據結構(即createContext
方法的返回值)也很簡單:
function createContext(defaultValue) {
const context = {
$$typeof: REACT_CONTEXT_TYPE,
Provider: null,
_currentValue: defaultValue
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
};
return context;
}
其中context._currentValue
保存context
當前值。
context
工作流程的三個步驟其實可以概括爲:
-
實例化
context
,並將默認值defaultValue
賦值給context._currentValue
-
每遇到一個同類型
context.Provier
,將value
賦值給context._currentValue
-
useContext(context)
就是簡單的取context._currentValue
的值就行
瞭解了工作流程後我們會發現,Context
的核心實現其實就是步驟 2。
核心實現
核心實現需要考慮什麼呢?還是以上面的示例爲例,當前只有一層<ctx.Provider>
包裹<Cpn />
:
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
在實際項目中,消費ctx
的組件(示例中的<Cpn/>
)可能被多級<ctx.Provider>
包裹,比如:
const ctx = createContext(0);
function App() {
return (
<ctx.Provider value={1}>
<ctx.Provider value={2}>
<ctx.Provider value={3}>
<Cpn />
</ctx.Provider>
<Cpn />
</ctx.Provider>
<Cpn />
</ctx.Provider>
);
}
在上面代碼中,ctx
的值會從 0(默認值)逐級變爲 3,再從 3 逐級變爲 0,所以沿途消費ctx
的<Cpn />
組件取得的值分別爲:3、2、1。
整個流程就像 「操作一個棧」,1、2、3 分別入棧,3、2、1 分別出棧,過程中棧頂的值就是context
當前的值。
基於此,context
的核心邏輯包括兩個函數:
function pushProvider(context, newValue) {
// ...
}
function popProvider(context) {
// ...
}
其中:
-
進入
ctx.Provider
時,執行pushProvider
方法,類比入棧操作 -
離開
ctx.Provider
時,執行popProvider
方法,類比出棧操作
每次執行pushProvider
時將context._currentValue
更新爲當前值:
function pushProvider(context, newValue) {
context._currentValue = newValue;
}
同理,popProvider
執行時將context._currentValue
更新爲上一個context._currentValue
:
function popProvider(context) {
context._currentValue = /* 上一個context value */
}
該如何表示上一個值呢?我們可以增加一個全局變量prevContextValue
,用於保存 「上一個同類型的 context._currentValue」:
let prevContextValue = null;
function pushProvider(context, newValue) {
// 保存上一個同類型context value
prevContextValue = context._currentValue;
context._currentValue = newValue;
}
function popProvider(context) {
context._currentValue = prevContextValue;
}
在pushProvider
中,執行如下語句前:
context._currentValue = newValue;
context._currentValue
中保存的就是 「上一個同類型的 context._currentValue」,將其賦值給prevContextValue
。
以下面代碼舉例:
const ctx = createContext(0);
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
進入ctx.Provider
時:
-
prevContextValue
賦值爲 0(context
實例化時傳遞的默認值) -
context._currentValue
賦值爲 1(當前值)
當<Cpn />
消費ctx
時,取得的值就是 1。
離開ctx.Provider
時:
context._currentValue
賦值爲 0(prevContextValue
對應值)
但是,我們當前的實現只能應對一層ctx.Provider
,如果是多層ctx.Provider
嵌套,我們不知道沿途ctx.Provider
對應的prevContextValue
。
所以,我們可以增加一個棧,用於保存沿途所有ctx.Provider
對應的prevContextValue
:
const prevContextValueStack = [];
let prevContextValue = null;
function pushProvider(context, newValue) {
prevContextValueStack.push(prevContextValue);
prevContextValue = context._currentValue;
context._currentValue = newValue;
}
function popProvider(context) {
context._currentValue = prevContextValue;
prevContextValue = prevContextValueStack.pop();
}
其中:
-
執行
pushProvider
時,讓prevContextValue
入棧 -
執行
popProvider
時,讓prevContextValue
出棧
至此,完成了React Context
的核心邏輯,其中pushProvider
三行代碼,popProvider
兩行代碼。
兩個有意思的點
關於Context
的實現,有兩個有意思的點。
第一個點:這個實現太過簡潔(核心就 5 行代碼),以至於讓人嚴重懷疑是不是有bug
?
比如,全局變量prevContextValue
用於保存 「上一個同類型的 context._currentValue」,如果我們把不同context
嵌套使用時會不會有問題?
在下面代碼中,ctxA
與ctxB
嵌套出現:
const ctxA = createContext('default A');
const ctxB = createContext('default B');
function App() {
return (
<ctxA.Provider value={'A0'}>
<ctxB.Provider value={'B0'}>
<ctxA.Provider value={'A1'}>
<Cpn />
</ctxA.Provider>
</ctxB.Provider>
<Cpn />
</ctxA.Provider>
);
}
當離開最內層ctxA.Provider
時,ctxA._currentValue
應該從'A1'
變爲'A0'
。考慮到prevContextValue
變量的唯一性以及棧的特性,ctxA._currentValue
會不會錯誤的變爲'B0'
?
答案是:不會。
JSX
結構的確定意味着以下兩點是確定的:
-
ctx.Provider
的進入與離開順序 -
多個
ctx.Provider
之間嵌套的順序
第一點保證了當進入與離開同一個ctx.Provider
時,prevContextValue
的值始終與該ctx
相關。
第二點保證了不同ctx.Provider
的prevContextValue
被以正確的順序入棧、出棧。
第二個有意思的點:我們知道,Hook
的使用有個限制 —— 不能在條件語句中使用hook
。
究其原因,對於同一個函數組件,Hook
的數據保存在一條鏈表上,所以必須保證遍歷鏈表時,鏈表數據與Hook
一一對應。
但我們發現,useContext
獲取的其實並不是鏈表數據,而是ctx._currentValue
,這意味着useContext
其實是不受這個限制影響的。
總結
以上五行代碼便是React Context
的核心實現。在實際的React
源碼中,Context
相關代碼遠不止五行,這是因爲他與其他特性耦合在一塊,比如:
-
性能優化相關代碼
-
SSR
相關代碼
所以,當我們面對複雜代碼時,不要輕言放棄。仔細分析下,沒準兒核心代碼只有幾行呢?
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qpQS3ne7HXSL5Dle-ts4qQ