<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在 React 中提供了一種「資料管理」機制:React.context
,大家可能對它比較陌生,日常開發直接使用它的場景也並不多。
但提起 react-redux
通過 Provider
將 store
中的全域性狀態在頂層元件向下傳遞,大家都不陌生,它就是基於 React 所提供的 context 特性實現。
本文,將從概念、使用,再到原理分析,來理解 Context 在多級元件之間進行資料傳遞的機制。
Context
提供了一個無需為每層元件手動新增 props,就能在元件樹間進行資料傳遞的方法。
通常,資料是通過 props 屬性自上而下(由父到子)進行傳遞,但這種做法對於某些型別的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程式中許多元件都需要的。
Context 提供了一種在元件之間共用此類值的方式,而不必顯式地通過元件樹的逐層傳遞 props。
設計目的是為了共用那些對於一個元件樹而言是“全域性”的資料,例如當前認證的使用者、主題或首選語言。
下面我們以 Hooks 函陣列件為例,展開介紹 Context 的使用。
首先,我們需要建立一個 React Context
物件。
const Context = React.createContext(defaultValue);
當 React 渲染一個訂閱了這個 Context 物件的元件,這個元件會從元件樹中的 Context.Provider 中讀取到當前的 context.value 值。
當元件所處的樹中沒有匹配到 Provider 時,其 defaultValue 引數才會生效。
每個 Context 物件都會返回一個 Provider React 元件,它接收一個 value 屬性,可將資料向下傳遞給消費元件。當 Provider 的 value 值發生變化時,它內部的所有消費元件都會重新渲染。
注意,當 value 傳遞為一個複雜物件時,若想要更新,必須賦予 value 一個新的物件參照地址,直接修改物件屬性不會觸發消費元件的重渲染。
<Context.Provider value={/* 某個值,一般會傳遞物件 */}>
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 發生變化,元件依舊會進行重渲染。
我們通過一個簡單範例來熟悉上述 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 元件內使用 Provider
將 value
值向子樹傳遞,Child 元件通過 useContext 讀取 value,從而成為 Consumer
消費元件。
從上面「使用」我們瞭解到:Context 的實現由三部分組成:
React.createContext()
方法;<Context.Provider value={value}>
;React.useContext(Context)
方法。原理分析脫離不了原始碼,下面我們挑選出核心程式碼來看看它們的實現。
createContext 原始碼定義在 react/src/ReactContext.js
位置。它返回一個 context
物件,提供了 Provider
和 Consumer
兩個元件屬性,_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; }
儘管在這裡我們只看到要返回一個物件,卻看不出別的名堂,只需記住它返回的物件結構資訊即可,我們接著往下看
我們所編寫的 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
節點。
在介紹 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; }
經過上面 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 } } } } }
通常,一個元件的更新可通過執行內部 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> ); } }
從「注意事項」可以考慮:要想使消費元件進行重渲染,context value
必須返回一個全新物件,這將導致所有消費元件都進行重渲染,這個開銷是非常大的,因為有一些元件所依賴的值可能並未發生變化。
當然有一種直觀做法是將「狀態」分離在不同 Context
之中。
react-redux useSelector
則是採用訂閱 redux store.state
更新,去通知消費元件「按需」進行重渲染(比較所依賴的 state 前後是否發生變化)。
Context.Provider
的 value 物件地址不會發生變化,這使得子元件中使用了 useSelector -> useContext
,但不會因頂層資料而進行重渲染。store.state
資料變化元件如何更新呢?react-redux
訂閱了 redux store.state
發生更新的動作,然後通知元件「按需」執行重渲染。以上就是react資料管理機制React.Context原始碼解析的詳細內容,更多關於React.Context 資料管理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45