React Context的核心實現(xiàn),就五行代碼
大家好,我卡頌。
很多項目的源碼非常復(fù)雜,讓人望而卻步。但在打退堂鼓前,我們應(yīng)該思考一個問題:源碼為什么復(fù)雜?
造成源碼復(fù)雜的原因不外乎有三個:
- 功能本身復(fù)雜,造成代碼復(fù)雜。
- 編寫者功力不行,寫的代碼復(fù)雜。
- 功能本身不復(fù)雜,但同一個模塊耦合了太多功能,看起來復(fù)雜。
如果是原因3,那實際理解起來其實并不難。我們需要的只是有人能幫我們剔除無關(guān)功能的干擾。
React Context的實現(xiàn)就是個典型例子,當(dāng)剔除無關(guān)功能的干擾后,他的核心實現(xiàn),僅需「5行代碼」。
本文就讓我們看看React Context的核心實現(xiàn)。
簡化模型
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數(shù)據(jù)結(jié)構(gòu)(即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當(dāng)前值。
context工作流程的三個步驟其實可以概括為:
- 實例化context,并將默認(rèn)值defaultValue賦值給context._currentValue。
- 每遇到一個同類型context.Provier,將value賦值給context._currentValue。
- useContext(context)就是簡單的取context._currentValue的值就行。
了解了工作流程后我們會發(fā)現(xiàn),Context的核心實現(xiàn)其實就是步驟2。
核心實現(xiàn)
核心實現(xiàn)需要考慮什么呢?還是以上面的示例為例,當(dāng)前只有一層<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(默認(rèn)值)逐級變?yōu)?,再從3逐級變?yōu)?,所以沿途消費ctx的<Cpn />組件取得的值分別為:3、2、1。
整個流程就像「操作一個?!?,1、2、3分別入棧,3、2、1分別出棧,過程中棧頂?shù)闹稻褪莄ontext當(dāng)前的值。
基于此,context的核心邏輯包括兩個函數(shù):
function pushProvider(context, newValue) {
// ...
}
function popProvider(context) {
// ...
}
其中:
- 進入ctx.Provider時,執(zhí)行pushProvider方法,類比入棧操作。
- 離開ctx.Provider時,執(zhí)行popProvider方法,類比出棧操作。
每次執(zhí)行pushProvider時將context._currentValue更新為當(dāng)前值:
function pushProvider(context, newValue) {
context._currentValue = newValue;
}
同理,popProvider執(zhí)行時將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中,執(zhí)行如下語句前:
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實例化時傳遞的默認(rèn)值)。
- context._currentValue賦值為1(當(dāng)前值)。
當(dāng)<Cpn />消費ctx時,取得的值就是1。
離開ctx.Provider時:
- context._currentValue賦值為0(prevContextValue對應(yīng)值)。
但是,我們當(dāng)前的實現(xiàn)只能應(yīng)對一層ctx.Provider,如果是多層ctx.Provider嵌套,我們不知道沿途ctx.Provider對應(yīng)的prevContextValue。
所以,我們可以增加一個棧,用于保存沿途所有ctx.Provider對應(yīng)的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();
}
其中:
- 執(zhí)行pushProvider時,讓prevContextValue入棧。
- 執(zhí)行popProvider時,讓prevContextValue出棧。
至此,完成了React Context的核心邏輯,其中pushProvider三行代碼,popProvider兩行代碼。
兩個有意思的點
關(guān)于Context的實現(xiàn),有兩個有意思的點。
第一個點:這個實現(xiàn)太過簡潔(核心就5行代碼),以至于讓人嚴(yán)重懷疑是不是有bug?
比如,全局變量prevContextValue用于保存「上一個同類型的context._currentValue」,如果我們把不同context嵌套使用時會不會有問題?
在下面代碼中,ctxA與ctxB嵌套出現(xiàn):
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>
);
}
當(dāng)離開最內(nèi)層ctxA.Provider時,ctxA._currentValue應(yīng)該從'A1'變?yōu)?A0'??紤]到prevContextValue變量的唯一性以及棧的特性,ctxA._currentValue會不會錯誤的變?yōu)?B0'?
答案是:不會。
JSX結(jié)構(gòu)的確定意味著以下兩點是確定的:
- ctx.Provider的進入與離開順序。
- 多個ctx.Provider之間嵌套的順序。
第一點保證了當(dāng)進入與離開同一個ctx.Provider時,prevContextValue的值始終與該ctx相關(guān)。
第二點保證了不同ctx.Provider的prevContextValue被以正確的順序入棧、出棧。
第二個有意思的點:我們知道,Hook的使用有個限制 —— 不能在條件語句中使用hook。
究其原因,對于同一個函數(shù)組件,Hook的數(shù)據(jù)保存在一條鏈表上,所以必須保證遍歷鏈表時,鏈表數(shù)據(jù)與Hook一一對應(yīng)。
但我們發(fā)現(xiàn),useContext獲取的其實并不是鏈表數(shù)據(jù),而是ctx._currentValue,這意味著useContext其實是不受這個限制影響的。
總結(jié)
以上五行代碼便是React Context的核心實現(xiàn)。在實際的React源碼中,Context相關(guān)代碼遠(yuǎn)不止五行,這是因為他與其他特性耦合在一塊,比如:
- 性能優(yōu)化相關(guān)代碼
- SSR相關(guān)代碼
所以,當(dāng)我們面對復(fù)雜代碼時,不要輕言放棄。仔細(xì)分析下,沒準(zhǔn)兒核心代碼只有幾行呢?