React Context最佳實踐加源碼解析

前言

在一個典型的React應用中, 數據都是經過props屬性自頂向下傳遞的, 也就是咱們一般所說的父傳子。可是在某些場景下(換膚), 不少底層的子組件都是須要接收來自於頂層組件的換膚屬性, 這會讓咱們的代碼中有不少顯示傳遞props的地方。Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。前端

什麼狀況下使用Context

Context的主要應用場景在於: 不一樣層級 的組件須要訪問相同的一些數據。node

下面咱們來看一個例子:react

場景是這樣的: 頁面的根組件是一個Page, 咱們須要向目標組件層層向下傳遞 useravatarSize, 從而深度嵌套的 AvatarInfo 組件能夠讀取到這些屬性。編程

<Page user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<PageLayout user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<NavigationBar user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<Avatar user={user} size={avatarSize} />
<Info user={user} size={avatarSize} />
複製代碼

若是最後只有Avatar和Info組件使用到了這兩個屬性, 那麼這種層層傳遞的方式會顯示十分冗餘。若是後面還須要新增相似colorbackground 等屬性, 咱們還得在中間層一個個地加上。markdown

在React官方文檔中, 提供了一種無需Context的方案, 使用 component composition(組件組合)在Page組件中將 AvatarInfo 組件傳遞下去。前端工程師

function Page(props) {
   const user = props.user;
   const userComponent = (
     <div> <Avatar user={user} size={props.avatarSize} /> <Info user={user} size={props.avatarSize} /> </div>
   );
   return <PageLayout userComponent={userLink} />;
 }

 <Page user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...
 <PageLayout userComponent={...} />
 // ... 渲染出 ...
 <NavigationBar userComponent={...} />
 // ... 渲染出 ...
 {props.userComponent}
複製代碼

經過上面這種方式, 能夠減小咱們React代碼中無用props的傳遞。可是這麼作有一個缺點: 頂層組件會變得十分複雜。ide

那麼這時候, Context是否是最佳的實踐方案呢?函數

答案是不必定。由於一旦你使用了Context, 組件的複用率將會變得很低。字體

在16.x以前的Context API有一個很很差點: 若是PageLayout 的props改變,可是在他的生命週期中 shouldComponentUpdate 返回的是false, 會致使 AvatarInfo的值沒法被更新。可是在16.x以後新版的 Context API, 就不會出現這個問題, 具體原理咱們在後面會講到, 此處先簡單的提一下。優化

若是咱們的這個組件不須要被複用, 那麼我以爲使用Context應該是目前的最佳實踐了。我以爲官網對Context的翻譯中,這句話講的特別好: Context 能讓你將這些數據向組件樹下全部的組件進行「廣播」,全部的組件都能訪問到這些數據,也能訪問到後續的數據更新 , 當我初學的時候, 我一看到這句話,就對Context有了一個清晰且深入的認識, 可是做爲一個前端工程師, 咱們的認知毫不停步於此。

import React, { Component, createContext, PureComponent } from 'react';

const SizeContext = createContext();

class User extends Component {
 render() {
   return <SizeContext.Consumer> { value => <span style={{ fontSize: value }}>我是其楓</span> } </SizeContext.Consumer>
 }
}

class PageLayout extends PureComponent {
 render() {
   return <NavigationBar />
 }
}

class NavigationBar extends PureComponent {
 render() {
   return <User/>
 }
}

class Page extends Component {
 state = {
   fontSize: 20
 }
 render() {
   const { fontSize } = this.state;
   return (
     <SizeContext.Provider value={fontSize}> <button type="button" onClick={() => this.setState({ fontSize: fontSize + 1 })} > 增長字體大小 </button> <button type="button" onClick={() => this.setState({ fontSize: fontSize - 1 })} > 減小字體大小 </button> <PageLayout /> </SizeContext.Provider>
   );
 }

}

export default Page;
複製代碼

經過使用Context, 可讓咱們的代碼變得更加的優雅。若是你們以爲這種寫法還不太優雅

<SizeContext.Consumer>
    {
      value => <span style={{ fontSize: value }}>我是其楓</span>
    }
  </SizeContext.Consumer> 
複製代碼

那麼咱們可使用 contextType 來進一步優化代碼

class User extends Component {
  static contextType = SizeContext;
  render() {
    return <span style={{ fontSize: this.context }}>我是其楓</span>
  }
}
複製代碼

注意: 它只支持單個Context, 若是多個的話仍是隻能使用嵌套的手法。

若是咱們想要定義多個Context, 好比新增一個顏色的Context的。咱們只需在Page組件中新增一個 Provider , 而且在消費方也新增一個 Consumer 便可。

<SizeContext.Provider value={fontSize}>
    <button type="button" onClick={() => this.setState({ fontSize: fontSize + 1 })} > 增長字體大小 </button>
    <button type="button" onClick={() => this.setState({ fontSize: fontSize - 1 })} > 減小字體大小 </button>
    <ColorContext.Provider value="red"> <PageLayout /> </ColorContext.Provider>
  </SizeContext.Provider>
複製代碼
<SizeContext.Consumer>
    {
      fontSize => <ColorContext.Consumer>{color => <span style={{ fontSize, color }}>我是其楓</span> }</ColorContext.Consumer>
    }
  </SizeContext.Consumer>
複製代碼

多個Context的消費方的編程體驗其實仍是不太友好的, Hook出現以後, 也推出了 useContext 這個API, 幫助咱們解決了這個問題。

const User = () => {
  const fontSize = useContext(SizeContext);
  const color = useContext(ColorContext);
  return (
    <span style={{ fontSize, color }}>我是其楓</span>
  );
};
複製代碼

在此, 我以爲有兩個點是值得咱們思考的:

  • 在16.X以後新版的Context是如何解決 相似shouldComponentUpdate的問題

  • Function Component是如何作到能夠訂閱多個Context的

接下來, 咱們將會帶着你們層層解開謎團, 探索新版Context的實現原理。

新版Context的實現原理

有源碼閱讀經驗的朋友應該對 ReactFiberBeginWork.js 下面的 beginWork 方法都不陌生吧。若是你以前沒有看過源碼也不要緊, 在本文你只須要知道這個方法是用來執行對整棵樹的每個節點進行更新的操做便可。那麼咱們的源碼解析就從 beginWork 開始。

Context的設計

var context = {
    ?typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider: null,
    Consumer: null
  };
複製代碼

咱們經過createContext會建立一個context對象, 該對象包含一個 Provider 以及 Consumer

Provider的_context指向是context自己。

context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};
複製代碼

下面咱們來看一下Consumer, 它的_context也是指向它自己

const Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
    _calculateChangedBits: context._calculateChangedBits,
  };
複製代碼

Provider的更新

當咱們第一次渲染的時候, 此時fiber樹上當前的節點確定是不存在的, 所以就不走 if (current !== null) { // ... } 這裏面的邏輯。咱們在建立 ColorContext.Provider 的時候, React會爲咱們的fiber節點打上一個 tag, 好比在此會被打上一個叫作 ContextProvider 的WorkTag。其餘節點也是相似, 目前一共有18種tag。

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
複製代碼

  • 首次更新Provider

當React處理Provider節點的時候, 會調用 updateContextProvider 方法進行Provider的更新

因爲是首次渲染, 因此當前fiber上的props是空的, 不存在 memoizedProps。所以咱們在這一步中僅僅執行了 pushProviderreconcileChildren。首先調用 pushProvider 將當前fiber以及它的props推入到棧(Stack)中。在React更新的過程當中有一個棧模塊(fiberStack), 在遍歷樹的時候, 它會存儲上下文。

在推入完成後, context的當前的值會置爲傳進來的值。

if (isPrimaryRenderer) {
  push(valueCursor, context._currentValue, providerFiber);
  context._currentValue = nextValue;
}
複製代碼

此時, Consumer上的值其實已經更新了。

固然若是執行 pushProvider 的時候發現不是第一次更新它會將 _currentValue2 的修改成最新的值。

push(valueCursor, context._currentValue2, providerFiber);
  context._currentValue2 = nextValue;
複製代碼

最後執行 reconcileChildren 將結果賦值給workInProgress.child。

  • 非首次更新Provider

當再次更新Provider的時候, 程序會進入 oldProps !== null

if (oldProps !== null) {
  const oldValue = oldProps.value;
  // 計算新老context上props的變化
  const changedBits = calculateChangedBits(context, newValue, oldValue);
  if (changedBits === 0) {
    // No change. Bailout early if children are the same.
    if (
      oldProps.children === newProps.children &&
      !hasLegacyContextChanged()
    ) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  } else {
    // The context value changed. Search for matching consumers and schedule
    // them to update.
    propagateContextChange(
      workInProgress,
      context,
      changedBits,
      renderExpirationTime,
    );
  }
}
複製代碼

程序是否更新取決於 calculateChangedBits計算後的值,

export function calculateChangedBits<T>( context: ReactContext<T>, newValue: T, oldValue: T, ) {
 if (
   (oldValue === newValue &&  
     (oldValue !== 0 || 1 / oldValue === 1 / (newValue: any))) || //排除 + 0 和 - 0
   (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare 排除NaN
 ) {
   // No change
   return 0;
 } else {
   const changedBits =
     typeof context._calculateChangedBits === 'function'
       ? context._calculateChangedBits(oldValue, newValue)
       : MAX_SIGNED_31_BIT_INT;

   return changedBits | 0;
 }
}
複製代碼

咱們能夠看到在這段代碼中, 若是props沒有變化,而且排除了 +0 和 -0 以及NaN 的狀況, 此時結果返回0。除此以外的其餘狀況都是要進行更新的。 節點的更新調用的是 propagateContextChange 這個函數

在首先渲染完成後, 咱們已經將子樹賦值給workInProgress.child。所以在第二次更新的時候, 咱們能夠直接經過 let fiber = workInProgress.child; 拿到子樹。

此時, 咱們經過遍歷fiber上的全部節點找到全部擁有 firstContextDependency的節點。

firstContextDependency 的初始化賦值在 readContext 方法中, 後面講到 Consumer的時候, 咱們會說起。

接着經過while循環判斷節點上依賴的context是否依賴當前context, 若是是React將會建立一個更新 createUpdate(), 而且打上 ForceUpdate的標籤表明強制更新, 最後把它塞到隊列裏面。爲了確保本次更新渲染週期內它必定會被執行, 咱們將 fiber上的 expirationTime的值改成當前正在執行更新的 expirationTime

最後更新workInProgress上的子樹。

Consumer的更新

Consumer的更新比較純粹, 它一共涉及三個主要階段: 1. prepareToReadContext(準備讀取Context) 2. readContext(開始讀取readContext) 3.子節點的渲染

  • prepareToReadContext

    在每次準備讀取Context的時候, React都會把樹上掛着的依賴給清空

    export function prepareToReadContext( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): void {
      currentlyRenderingFiber = workInProgress;
      lastContextDependency = null;
      lastContextWithAllBitsObserved = null;
    
      // Reset the work-in-progress list
      workInProgress.firstContextDependency = null;
    }
    複製代碼
  • readContext

    在readContext中, 經過鏈表存儲的形式, 將fiber上全部依賴的context都記錄下來。在下一次更新後, Provider那邊就能夠經過fiber節點, 拿到它所依賴的context了。

Hook下useContext的解析

Hook下的全部鉤子都是經過dispatch派發出來的

const HooksDispatcherOnRerender: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  useDebugValue: updateDebugValue,
  useDeferredValue: rerenderDeferredValue,
  useTransition: rerenderTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: rerenderOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};
複製代碼

在上述的代碼中, 在使用Hook的同時, 其實是調用了 readContext 這個API。它直接調用了底層的API, 讓咱們能夠直接得到最新context的值。

在咱們以前執行 pushProvider 的時候會分別將值賦值給context的 _currentValue _currentValue2. 所以當咱們調用useContext的時候readContext 返回的已經當前context的最新值了。

function readContext() {
  // ...
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
複製代碼
相關文章
相關標籤/搜索