日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React?Context原理深入理解源碼示例分析_React

作者:flyzz177 ? 更新時間: 2023-02-09 編程語言

正文

在 React 中提供了一種「數據管理」機制:React.context,大家可能對它比較陌生,日常開發直接使用它的場景也并不多。

但提起 react-redux 通過 Providerstore 中的全局狀態在頂層組件向下傳遞,大家都不陌生,它就是基于 React 所提供的 context 特性實現。

本文,將從概念、使用,再到原理分析,來理解 Context 在多級組件之間進行數據傳遞的機制。

一、概念

Context 提供了一個無需為每層組件手動添加 props,就能在組件樹間進行數據傳遞的方法。

通常,數據是通過 props 屬性自上而下(由父到子)進行傳遞,但這種做法對于某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都需要的。

Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞 props。

設計目的是為了共享那些對于一個組件樹而言是“全局”的數據,例如當前認證的用戶、主題或首選語言。

二、使用

下面我們以 Hooks 函數組件為例,展開介紹 Context 的使用。

2.1、React.createContext

首先,我們需要創建一個 React Context 對象。

const Context = React.createContext(defaultValue);

當 React 渲染一個訂閱了這個 Context 對象的組件,這個組件會從組件樹中的 Context.Provider 中讀取到當前的 context.value 值。

當組件所處的樹中沒有匹配到 Provider 時,其 defaultValue 參數才會生效。

2.2、Context.Provider

每個 Context 對象都會返回一個 Provider React 組件,它接收一個 value 屬性,可將數據向下傳遞給消費組件。當 Provider 的 value 值發生變化時,它內部的所有消費組件都會重新渲染。

注意,當 value 傳遞為一個復雜對象時,若想要更新,必須賦予 value 一個新的對象引用地址,直接修改對象屬性不會觸發消費組件的重渲染。

<Context.Provider value={/* 某個值,一般會傳遞對象 */}>

2.3、React.useContext

Context Provider 組件提供了向下傳遞的 value 數據,對于函數組件,可通過 useContext API 拿到 Context value

const value = useContext(Context);

useContext 接收一個 context 對象(React.createContext 的返回值),返回該 context 的當前值。

當組件上層最近的 <Context.Provider> 更新時,當前組件會觸發重渲染,并讀取最新傳遞給 Context Provider 的 context value 值。

題外話:React.memo 只會針對 props 做優化,如果組件中 useContext 依賴的 context value 發生變化,組件依舊會進行重渲染。

2.4、Example

我們通過一個簡單示例來熟悉上述 Context 的使用。

const Context = React.createContext(null);
const Child = () => {
  const value = React.useContext(Context);
  return (
    <div>theme: {value.theme}</div>
  )
}
const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <Context.Provider value={{ theme: 'light' }}>
      <div onClick={() => setCount(count + 1)}>觸發更新</div>
      <Child />
    </Context.Provider>
  )
}
ReactDOM.render(<App />, document.getElementById('root'));

示例中,在 App 組件內使用 Providervalue 值向子樹傳遞,Child 組件通過 useContext 讀取 value,從而成為 Consumer 消費組件。

三、原理分析

從上面「使用」我們了解到:Context 的實現由三部分組成:

  • 創建 Context:React.createContext() 方法;
  • Provider 組件:<Context.Provider value={value}>
  • 消費 value:React.useContext(Context) 方法。

原理分析脫離不了源碼,下面我們挑選出核心代碼來看看它們的實現。

3.1、createContext 函數實現

createContext 源碼定義在 react/src/ReactContext.js 位置。它返回一個 context 對象,提供了 ProviderConsumer 兩個組件屬性,_currentValue 會保存 context.value 值。

const REACT_PROVIDER_TYPE = Symbol.for('react.provider');
const REACT_CONTEXT_TYPE = Symbol.for('react.context');
export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    // 并發渲染器方案,分為主渲染器和輔助渲染器
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0, // 跟蹤此上下文當前有多少個并發渲染器
    Provider: (null: any),
    Consumer: (null: any),
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}

盡管在這里我們只看到要返回一個對象,卻看不出別的名堂,只需記住它返回的對象結構信息即可,我們接著往下看。

3.2、 JSX 編譯

我們所編寫的 JSX 語法在進入 render 時會被 babel 編譯成 ReactElement 對象。我們可以在 babel repl 在線平臺 轉換查看。

JSX 語法最終會被轉換成 React.createElement 方法,我們在 example 環境下執行方法,返回的結果是一個 ReactElement 元素對象。

對象的 props 保存了 context 要向下傳遞的 value,而對象的 type 則保存的是 context.Provider

context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};

有了對象描述結構,接下來進入渲染流程并在 Reconciler/beginWork 階段為其創建 Fiber 節點。

3.3、消費組件 - useContext 函數實現

在介紹 Provider Fiber 節點處理前,我們需要先了解下 Consumer 消費組件如何使用 context value,以便于更好理解 Provider 的實現。

useContext 接收 context 對象作為參數,從 context._currentValue 中讀取 value 值。

不過,除了讀取 value 值外,還會將 context 信息保存在當前組件 Fiber.dependencies 上。

目的是為了在 Provider value 發生更新時,可以查找到消費組件并標記上更新,執行組件的重渲染邏輯。

function useContext(Context) {
  // 將 context 記錄在當前 Fiber.dependencies 節點上,在 Provider 檢測到 value 更新后,會查找消費組件標記更新。
  const contextItem = {
    context: context,
    next: null, // 一個組件可能注冊多個不同的 context
  };
  if (lastContextDependency === null) {
    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
      responders: null
    };
  } else {
    // Append a new context item.
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return context._currentValue;
}

3.4、Context.Provider 在 Fiber 架構下的實現機制

經過上面 useContext 消費組件的分析,我們需要思考兩點:

  • <Provider> 組件上的 value 值何時更新到 context._currentValue
  • Provider.value 值發生更新后,如果能夠讓消費組件進行重渲染 ?

這兩點都會在這里找到答案。

在 example 中,點擊「觸發更新」div 后,React 會進入調度更新階段。我們通過斷點定位到 Context.Provider Fiber 節點的 Reconciler/beginWork 之中。

Provider Fiber 類型為 ContextProvider,因此進入 tag switch case 中的 updateContextProvider

function beginWork(current, workInProgress, renderLanes) {
  ...
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}

首先,更新 context._currentValue,比較新老 value 是否發生變化。

注意,這里使用的是 Object.is,通常我們傳遞的 value 都是一個復雜對象類型,它將比較兩個對象的引用地址是否相同。

若引用地址未發生變化,則會進入 bailout 復用當前 Fiber 節點。

在 bailout 中,會檢查該 Fiber 的所有子孫 Fiber 是否存在 lane 更新。若所有子孫 Fiber 本次都沒有更新需要執行,則 bailout 會直接返回 null,整棵子樹都被跳過更新。

function updateContextProvider(current, workInProgress, renderLanes) {
  var providerType = workInProgress.type;
  var context = providerType._context;
  var newProps = workInProgress.pendingProps;
  var oldProps = workInProgress.memoizedProps;
  var newValue = newProps.value;
  var oldValue = oldProps.value;
  // 1、更新 value prop 到 context 中
  context._currentValue = nextValue;
  // 2、比較前后 value 是否有變化,這里使用 Object.is 進行比較(對于對象,僅比較引用地址是否相同)
  if (objectIs(oldValue, newValue)) {
    // children 也相同,進入 bailout,結束子樹的協調
    if (oldProps.children === newProps.children && !hasContextChanged()) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {
    // 3、context value 發生變化,深度優先遍歷查找 consumer 消費組件,標記更新
    propagateContextChange(workInProgress, context, changedBits, renderLanes);
  }
  // ... reconciler children
}

context.value 發生變化,調用 propagateContextChange 對 Fiber 子樹向下深度優先遍歷,目的是為了查找 Context 消費組件,并為其標記 lane 更新,即讓其后續進入 Reconciler/beginWork 階段后不滿足 bailout 條件 !includesSomeLane(renderLanes, updateLanes)

function propagateContextChange(workInProgress, context, changedBits, renderLanes) {
  var fiber = workInProgress.child;
  while (fiber !== null) {
    var nextFiber;
    var list = fiber.dependencies; // 若 fiber 屬于一個 Consumer 組件,dependencies 上記錄了 context 對象
    if (list !== null) {
      var dependency = list.firstContext; // 拿出第一個 context
      while (dependency !== null) {
        // Check if the context matches.
        if (dependency.context === context) {
          if (fiber.tag === ClassComponent) {
            var update = createUpdate(NoTimestamp, pickArbitraryLane(renderLanes));
            update.tag = ForceUpdate;
            enqueueUpdate(fiber, update);
          }
          // 標記組件存在更新,!includesSomeLane(renderLanes, updateLanes) 
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          // 在上層 Fiber 樹的節點上標記 childLanes 存在更新
          scheduleWorkOnParentPath(fiber.return, renderLanes);
          ...
          break
        }
      }
    }
  }
}

3.5、小結

通常,一個組件的更新可通過執行內部 setState 來生成,其方式也是標記 Fiber.lane 讓組件不進入 bailout;

對于 Context,當 Provider.value 發生更新后,它會查找子樹找到消費組件,為消費組件的 Fiber 節點標記 lane。

當組件(函數組件)進入 Reconciler/beginWork 階段進行處理時,不滿足 bailout,就會重新被調用進行重渲染,這時執行 useContext,就會拿到最新的 context.__currentValue

這就是 React.context 實現過程。

四、注意事項

React 性能一大關鍵在于,減少不必要的 render。Context 會通過 Object.is(),即 === 來比較前后 value 是否嚴格相等。這里可能會有一些陷阱:當注冊 Provider 的父組件進行重渲染時,會導致消費組件觸發意外渲染。

如下例子,當每一次 Provider 重渲染時,以下的代碼會重渲染所有消費組件,因為 value 屬性總是被賦值為新的對象:

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

為了防止這種情況,可以將 value 狀態提升到父節點的 state 里:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: { something: 'something' },
    };
  }
  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

五、對比 useSelector

從「注意事項」可以考慮:要想使消費組件進行重渲染,context value 必須返回一個全新對象,這將導致所有消費組件都進行重渲染,這個開銷是非常大的,因為有一些組件所依賴的值可能并未發生變化。

當然有一種直觀做法是將「狀態」分離在不同 Context 之中。

react-redux useSelector 則是采用訂閱 redux store.state 更新,去通知消費組件「按需」進行重渲染(比較所依賴的 state 前后是否發生變化)。

  • 提供給 Context.Provider 的 value 對象地址不會發生變化,這使得子組件中使用了 useSelector -> useContext,但不會因頂層數據而進行重渲染。
  • store.state 數據變化組件如何更新呢?react-redux 訂閱了 redux store.state 發生更新的動作,然后通知組件「按需」執行重渲染。

原文鏈接:https://juejin.cn/post/7184253495116202043

欄目分類
最近更新