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