banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useContext 和 Context

說明#

  1. 本文重點將會分析 React 內的 Context 機制,分為三個小階段
    1. createContext
    2. context.Provider
    3. useContext
  2. demo 基於官方例子
  const themes = {
    light: {
      foreground: "#000000",
      background: "#eeeeee"
    },
    dark: {
      foreground: "#ffffff",
      background: "#222222"
    }
  };

  const ThemeContext = React.createContext(themes.light);

  function App() {
    return (
      <ThemeContext.Provider value={themes.dark}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }

  function Toolbar(props) {
    return (
      <div>
        <ThemedButton />
      </div>
    );
  }

  function ThemedButton() {
    const theme = useContext(ThemeContext);
    return (
      <button style={{ background: theme.background, color: theme.foreground }}>
        我是由主題上下文樣式化的!
      </button>
    );
  }

createContext#

直接對 createContext 打上斷點,

  export function createContext<T>(defaultValue: T): ReactContext<T> {
  /**
   * 聲明了一個 context
  */
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,

    _currentValue: defaultValue,
    _currentValue2: defaultValue,

    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),

    // Add these to use same hidden class in VM as ServerContext
    _defaultValue: (null: any),
    _globalName: (null: any),
  };

  /**
   * beginWork 創建 fiber 時會根據 $$typeof 來分別處理
  */
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;
  
  context.Consumer = context;

  return context;
}

東西不多,內部主要是聲明了一個 context 對象,並在上面掛上了 Provider 的屬性。

image

接下來看看 provider 裡面是怎麼處理的

context.Provider#

仔細看 demo 裡面用到 Provider 的地方,能發現它是被以 jsx 的方式使用到程序內的。

那麼調試的思路也很明顯了,去觀察 beginWork 內生成 fiber 的地方

對 App 的 return 打點,會進入以下調用棧

image

在 createFiberFromTypeAndProps 內會先將 ContextProvider 賦值給 fiberTag,以表示我需要創建一個 Provider 特供的 fiber 節點,然後進入 createFiber,隨後便返回了一個 type 為 ContextProvider 的 fiber 節點。

image

需要注意的是此時我們還在 App 的 beginWork 內,如果需要觀察 ContextProvider 的,需要進入下一次 beginWork。

image

這裡才是正式處理 ContextProvider 的地方。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  // createContext 創建的 context 對象
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;
  
  // 這裡修改了 context 的 _currentValue為新值
  pushProvider(workInProgress, context, newValue);

  if (oldProps !== null) {
    const oldValue = oldProps.value;
    // provider 比較 value前後是否發生變化
    if (is(oldValue, newValue)) {
      // No change. Bailout early if children are the same.
      // 沒有改變的話,並且 children 未更新就復用原來的 fiber 然後退出
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      }
    } else {
      // 如果前後 value 不同,那麼就更新所有 consumers
      // 這裡暫時先不展開,等到 useContext 時再介紹
      // The context value changed. Search for matching consumers and schedule
      // them to update.
      propagateContextChange(workInProgress, context, renderLanes);
    }
  }
  
  // 繼續創建 provider 的子節點
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

updateContextProvider 的思路也比較簡單,修改 context 內的 value 後比較新舊值是否相等,相等就復用 fiber 節點,不相等就重新創建新的 fiber 節點。唯獨需要注意的地方是 propagateContextChange。這個我們會結合 useContext 一塊講。

useContext#

令人高興的是,這個 hook 並不需要分 mount 與 update 來講。並且也不會生成 hook 對象。

image

我們直接來看 readContext 裡面做了什麼就行了。

因為使用這個 hook 時需要傳入對應的 context 對象

  const theme = useContext(ThemeContext);
export function readContext<T>(context: ReactContext<T>): T {
  /**
   * 獲取 context 傳入的 value
   * */  
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    // 創建了一個 contextItem,後面會穿成鏈表掛在目前正在渲染的 fiber 節點上
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };

    if (lastContextDependency === null) {
      // 創建鏈表,並掛載在 fiber 節點上
      // This is the first dependency for this component. Create a new list.
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        currentlyRenderingFiber.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      // 在 fiber 上的 context item 鏈表上加元素
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  // 把值丟出去
  return value;
}

此時大致上就是這麼個結構了

image

readContext 主要做的還是讀取傳入 context 內的值,但是為什麼要在當前渲染的 fiber 節點上增加一個與 context 相關的鏈表呢?

這時需要回過頭看看 propagateContextChange 了

  function propagateContextChange_eager(workInProgress, context, renderLanes) {

    var fiber = workInProgress.child;

    if (fiber !== null) {
      // Set the return pointer of the child to the work-in-progress fiber.
      // 給子節點綁定 父節點
      fiber.return = workInProgress;
    }

    while (fiber !== null) {
      var nextFiber = void 0; // Visit this fiber.
      /** 
       * fiber 節點上的 dependencies 在這裡被視為了內部存在使用了 context 的標識
      */
      var list = fiber.dependencies;

      if (list !== null) {
        // 這裡是有使用到 context 的場景
        nextFiber = fiber.child;
        var dependency = list.firstContext;

        /**
         * 這裡通過 while 遍歷 context item 的鏈表確認是否存在當前 provider 上綁定的context 對象
        */
        while (dependency !== null) {
          // Check if the context matches.
          if (dependency.context === context) {
            // 找到了有使用到當前 provider 上標記的 context 對象
            // Match! Schedule an update on this fiber.
            if (fiber.tag === ClassComponent) {
              ...
            }
            // 給當前 fiber 標記上更新標識
            fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
            var alternate = fiber.alternate;
            // 同樣也需要給當前 WIP 對應的 current 節點也打上相同的標記,防止被更高優先級任務插隊後丟失信息
            if (alternate !== null) {
              alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
            }
            /**
             * 這裡在別的文章裡也多次提到過類似的操作(比如 useState)
             * 其實就是模擬了生成了一個 renderlanes 的 update
             * 將更新從當前節點一直向上標記,直到 root 節點。
             * 從而在調度時一起更新
            */
            scheduleContextWorkOnParentPath(fiber.return, renderLanes, workInProgress); // Mark the updated lanes on the list, too.

            list.lanes = mergeLanes(list.lanes, renderLanes); // Since we already found a match, we can stop traversing the
            // dependency list.

            break;
          }

          dependency = dependency.next;
        }

      } else if (fiber.tag === ContextProvider) {
        ...
      } else if (fiber.tag === DehydratedFragment) {
        ...
      } else {
        ...
      }

      ...
      }

      fiber = nextFiber;
    }
  }

image

大致總結一下便是,每一個使用到了 context 對象的 fiber 節點都會被打上 dependencies 的鏈表屬性,一旦 provider 上的 value 發生改變,它就會遍歷自己的所有子節點是否擁有 dependencies 的標記,並給他們打上更新標記從而實現強制更新。就像文檔裡說的一樣。

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。