React Context 的核心實現,就 5 行代碼

大家好,我卡頌。

很多項目的源碼非常複雜,讓人望而卻步。但在打退堂鼓前,我們應該思考一個問題:源碼爲什麼複雜?

造成源碼複雜的原因不外乎有三個:

  1. 功能本身複雜,造成代碼複雜

  2. 編寫者功力不行,寫的代碼複雜

  3. 功能本身不復雜,但同一個模塊耦合了太多功能,看起來複雜

如果是原因 3,那實際理解起來其實並不難。我們需要的只是有人能幫我們剔除無關功能的干擾。

React Context的實現就是個典型例子,當剔除無關功能的干擾後,他的核心實現,僅需 「5 行代碼」

本文就讓我們看看React Context的核心實現。

簡化模型

Context的完整工作流程包括 3 步:

  1. 定義context

  2. 賦值context

  3. 消費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>;
}

其中:

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工作流程的三個步驟其實可以概括爲:

  1. 實例化context,並將默認值defaultValue賦值給context._currentValue

  2. 每遇到一個同類型context.Provier,將value賦值給context._currentValue

  3. 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) {
 // ...
}

其中:

每次執行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時:

<Cpn />消費ctx時,取得的值就是 1。

離開ctx.Provider時:

但是,我們當前的實現只能應對一層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();
}

其中:

至此,完成了React Context的核心邏輯,其中pushProvider三行代碼,popProvider兩行代碼。

兩個有意思的點

關於Context的實現,有兩個有意思的點。

第一個點:這個實現太過簡潔(核心就 5 行代碼),以至於讓人嚴重懷疑是不是有bug

比如,全局變量prevContextValue用於保存 「上一個同類型的 context._currentValue」,如果我們把不同context嵌套使用時會不會有問題?

在下面代碼中,ctxActxB嵌套出現:

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結構的確定意味着以下兩點是確定的:

  1. ctx.Provider的進入與離開順序

  2. 多個ctx.Provider之間嵌套的順序

第一點保證了當進入與離開同一個ctx.Provider時,prevContextValue的值始終與該ctx相關。

第二點保證了不同ctx.ProviderprevContextValue被以正確的順序入棧、出棧。

第二個有意思的點:我們知道,Hook的使用有個限制 —— 不能在條件語句中使用hook

究其原因,對於同一個函數組件,Hook的數據保存在一條鏈表上,所以必須保證遍歷鏈表時,鏈表數據與Hook一一對應。

但我們發現,useContext獲取的其實並不是鏈表數據,而是ctx._currentValue,這意味着useContext其實是不受這個限制影響的。

總結

以上五行代碼便是React Context的核心實現。在實際的React源碼中,Context相關代碼遠不止五行,這是因爲他與其他特性耦合在一塊,比如:

所以,當我們面對複雜代碼時,不要輕言放棄。仔細分析下,沒準兒核心代碼只有幾行呢?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qpQS3ne7HXSL5Dle-ts4qQ