網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
開(kāi)篇
在 React 中提供了一種「數(shù)據(jù)管理」機(jī)制:React.context
,大家可能對(duì)它比較陌生,日常開(kāi)發(fā)直接使用它的場(chǎng)景也并不多。
但提起 react-redux
通過(guò) Provider
將 store
中的全局狀態(tài)在頂層組件向下傳遞,大家都不陌生,它就是基于 React 所提供的 context 特性實(shí)現(xiàn)。
本文,將從概念、使用,再到原理分析,來(lái)理解 Context 在多級(jí)組件之間進(jìn)行數(shù)據(jù)傳遞的機(jī)制。
一、概念
Context
提供了一個(gè)無(wú)需為每層組件手動(dòng)添加 props,就能在組件樹(shù)間進(jìn)行數(shù)據(jù)傳遞的方法。
通常,數(shù)據(jù)是通過(guò) props 屬性自上而下(由父到子)進(jìn)行傳遞,但這種做法對(duì)于某些類型的屬性而言是極其繁瑣的(例如:地區(qū)偏好,UI 主題),這些屬性是應(yīng)用程序中許多組件都需要的。
Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過(guò)組件樹(shù)的逐層傳遞 props。
設(shè)計(jì)目的是為了共享那些對(duì)于一個(gè)組件樹(shù)而言是“全局”的數(shù)據(jù),例如當(dāng)前認(rèn)證的用戶、主題或首選語(yǔ)言。
二、使用
下面我們以 Hooks 函數(shù)組件為例,展開(kāi)介紹 Context 的使用。
2.1、React.createContext
首先,我們需要?jiǎng)?chuàng)建一個(gè) React Context
對(duì)象。
const Context = React.createContext(defaultValue);
當(dāng) React 渲染一個(gè)訂閱了這個(gè) Context 對(duì)象的組件,這個(gè)組件會(huì)從組件樹(shù)中的 Context.Provider 中讀取到當(dāng)前的 context.value 值。
當(dāng)組件所處的樹(shù)中沒(méi)有匹配到 Provider 時(shí),其 defaultValue 參數(shù)才會(huì)生效。
2.2、Context.Provider
每個(gè) Context 對(duì)象都會(huì)返回一個(gè) Provider React 組件,它接收一個(gè) value 屬性,可將數(shù)據(jù)向下傳遞給消費(fèi)組件。當(dāng) Provider 的 value 值發(fā)生變化時(shí),它內(nèi)部的所有消費(fèi)組件都會(huì)重新渲染。
注意,當(dāng) value 傳遞為一個(gè)復(fù)雜對(duì)象時(shí),若想要更新,必須賦予 value 一個(gè)新的對(duì)象引用地址,直接修改對(duì)象屬性不會(huì)觸發(fā)消費(fèi)組件的重渲染。
<Context.Provider value={/* 某個(gè)值,一般會(huì)傳遞對(duì)象 */}>
2.3、React.useContext
Context Provider
組件提供了向下傳遞的 value
數(shù)據(jù),對(duì)于函數(shù)組件,可通過(guò) useContext
API 拿到 Context value
。
const value = useContext(Context);
useContext
接收一個(gè) context 對(duì)象(React.createContext 的返回值),返回該 context 的當(dāng)前值。
當(dāng)組件上層最近的 <Context.Provider> 更新時(shí),當(dāng)前組件會(huì)觸發(fā)重渲染,并讀取最新傳遞給 Context Provider 的 context value 值。
題外話:React.memo 只會(huì)針對(duì) props 做優(yōu)化,如果組件中 useContext 依賴的 context value 發(fā)生變化,組件依舊會(huì)進(jìn)行重渲染。
2.4、Example
我們通過(guò)一個(gè)簡(jiǎn)單示例來(lái)熟悉上述 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)}>觸發(fā)更新</div> <Child /> </Context.Provider> ) } ReactDOM.render(<App />, document.getElementById('root'));
示例中,在 App 組件內(nèi)使用 Provider
將 value
值向子樹(shù)傳遞,Child 組件通過(guò) useContext 讀取 value,從而成為 Consumer
消費(fèi)組件。
三、原理分析
從上面「使用」我們了解到:Context 的實(shí)現(xiàn)由三部分組成:
- 創(chuàng)建 Context:
React.createContext()
方法; - Provider 組件:
<Context.Provider value={value}>
; - 消費(fèi) value:
React.useContext(Context)
方法。
原理分析脫離不了源碼,下面我們挑選出核心代碼來(lái)看看它們的實(shí)現(xiàn)。
3.1、createContext 函數(shù)實(shí)現(xiàn)
createContext 源碼定義在 react/src/ReactContext.js
位置。它返回一個(gè) context
對(duì)象,提供了 Provider
和 Consumer
兩個(gè)組件屬性,_currentValue
會(huì)保存 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, // 并發(fā)渲染器方案,分為主渲染器和輔助渲染器 _currentValue: defaultValue, _currentValue2: defaultValue, _threadCount: 0, // 跟蹤此上下文當(dāng)前有多少個(gè)并發(fā)渲染器 Provider: (null: any), Consumer: (null: any), }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; context.Consumer = context; return context; }
盡管在這里我們只看到要返回一個(gè)對(duì)象,卻看不出別的名堂,只需記住它返回的對(duì)象結(jié)構(gòu)信息即可,我們接著往下看
3.2、 JSX 編譯
我們所編寫(xiě)的 JSX 語(yǔ)法在進(jìn)入 render 時(shí)會(huì)被 babel
編譯成 ReactElement
對(duì)象。我們可以在 babel repl 在線平臺(tái) 轉(zhuǎn)換查看。
JSX 語(yǔ)法最終會(huì)被轉(zhuǎn)換成 React.createElement
方法,我們?cè)?example 環(huán)境下執(zhí)行方法,返回的結(jié)果是一個(gè) ReactElement
元素對(duì)象。
對(duì)象的 props
保存了 context 要向下傳遞的 value
,而對(duì)象的 type
則保存的是 context.Provider
。
context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, };
有了對(duì)象描述結(jié)構(gòu),接下來(lái)進(jìn)入渲染流程并在 Reconciler/beginWork
階段為其創(chuàng)建 Fiber
節(jié)點(diǎn)。
3.3、消費(fèi)組件 - useContext 函數(shù)實(shí)現(xiàn)
在介紹 Provider Fiber
節(jié)點(diǎn)處理前,我們需要先了解下 Consumer
消費(fèi)組件如何使用 context value
,以便于更好理解 Provider
的實(shí)現(xiàn)。
useContext
接收 context
對(duì)象作為參數(shù),從 context._currentValue
中讀取 value 值。
不過(guò),除了讀取 value 值外,還會(huì)將 context 信息保存在當(dāng)前組件 Fiber.dependencies
上。
目的是為了在 Provider value
發(fā)生更新時(shí),可以查找到消費(fèi)組件并標(biāo)記上更新,執(zhí)行組件的重渲染邏輯。
function useContext(Context) { // 將 context 記錄在當(dāng)前 Fiber.dependencies 節(jié)點(diǎn)上,在 Provider 檢測(cè)到 value 更新后,會(huì)查找消費(fèi)組件標(biāo)記更新。 const contextItem = { context: context, next: null, // 一個(gè)組件可能注冊(cè)多個(gè)不同的 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 架構(gòu)下的實(shí)現(xiàn)機(jī)制
經(jīng)過(guò)上面 useContext
消費(fèi)組件的分析,我們需要思考兩點(diǎn):
-
<Provider>
組件上的 value 值何時(shí)更新到context._currentValue
? -
Provider.value
值發(fā)生更新后,如果能夠讓消費(fèi)組件進(jìn)行重渲染 ?
這兩點(diǎn)都會(huì)在這里找到答案。
在 example 中,點(diǎn)擊「觸發(fā)更新」div 后,React 會(huì)進(jìn)入調(diào)度更新階段。我們通過(guò)斷點(diǎn)定位到 Context.Provider
Fiber 節(jié)點(diǎn)的 Reconciler/beginWork
之中。
Provider Fiber 類型為 ContextProvider
,因此進(jìn)入 tag switch case 中的 updateContextProvider
。
function beginWork(current, workInProgress, renderLanes) { ... switch (workInProgress.tag) { case ContextProvider: return updateContextProvider(current, workInProgress, renderLanes); } }
首先,更新 context._currentValue
,比較新老 value 是否發(fā)生變化。
注意,這里使用的是 Object.is
,通常我們傳遞的 value 都是一個(gè)復(fù)雜對(duì)象類型,它將比較兩個(gè)對(duì)象的引用地址是否相同。
若引用地址未發(fā)生變化,則會(huì)進(jìn)入 bailout
復(fù)用當(dāng)前 Fiber 節(jié)點(diǎn)。
在 bailout 中,會(huì)檢查該 Fiber 的所有子孫 Fiber 是否存在 lane 更新。若所有子孫 Fiber 本次都沒(méi)有更新需要執(zhí)行,則 bailout 會(huì)直接返回 null,整棵子樹(shù)都被跳過(guò)更新。
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 進(jìn)行比較(對(duì)于對(duì)象,僅比較引用地址是否相同) if (objectIs(oldValue, newValue)) { // children 也相同,進(jìn)入 bailout,結(jié)束子樹(shù)的協(xié)調(diào) if (oldProps.children === newProps.children && !hasContextChanged()) { return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } else { // 3、context value 發(fā)生變化,深度優(yōu)先遍歷查找 consumer 消費(fèi)組件,標(biāo)記更新 propagateContextChange(workInProgress, context, changedBits, renderLanes); } // ... reconciler children }
若 context.value
發(fā)生變化,調(diào)用 propagateContextChange
對(duì) Fiber 子樹(shù)向下深度優(yōu)先遍歷,目的是為了查找 Context
消費(fèi)組件,并為其標(biāo)記 lane 更新,即讓其后續(xù)進(jìn)入 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 屬于一個(gè) Consumer 組件,dependencies 上記錄了 context 對(duì)象 if (list !== null) { var dependency = list.firstContext; // 拿出第一個(gè) 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); } // 標(biāo)記組件存在更新,!includesSomeLane(renderLanes, updateLanes) fiber.lanes = mergeLanes(fiber.lanes, renderLanes); // 在上層 Fiber 樹(shù)的節(jié)點(diǎn)上標(biāo)記 childLanes 存在更新 scheduleWorkOnParentPath(fiber.return, renderLanes); ... break } } } } }
3.5、總結(jié)
通常,一個(gè)組件的更新可通過(guò)執(zhí)行內(nèi)部 setState 來(lái)生成,其方式也是標(biāo)記 Fiber.lane
讓組件不進(jìn)入 bailout;
對(duì)于 Context
,當(dāng) Provider.value 發(fā)生更新后,它會(huì)查找子樹(shù)找到消費(fèi)組件,為消費(fèi)組件的 Fiber 節(jié)點(diǎn)標(biāo)記 lane。
當(dāng)組件(函數(shù)組件)進(jìn)入 Reconciler/beginWork
階段進(jìn)行處理時(shí),不滿足 bailout
,就會(huì)重新被調(diào)用進(jìn)行重渲染,這時(shí)執(zhí)行 useContext
,就會(huì)拿到最新的 context.__currentValue
。
這就是 React.context
實(shí)現(xiàn)過(guò)程。
四、注意事項(xiàng)
React 性能一大關(guān)鍵在于,減少不必要的 render。Context 會(huì)通過(guò) Object.is()
,即 ===
來(lái)比較前后 value 是否嚴(yán)格相等。這里可能會(huì)有一些陷阱:當(dāng)注冊(cè) Provider 的父組件進(jìn)行重渲染時(shí),會(huì)導(dǎo)致消費(fèi)組件觸發(fā)意外渲染。
如下例子,當(dāng)每一次 Provider 重渲染時(shí),以下的代碼會(huì)重渲染所有消費(fèi)組件,因?yàn)?value 屬性總是被賦值為新的對(duì)象:
class App extends React.Component { render() { return ( <MyContext.Provider value={{something: 'something'}}> <Toolbar /> </MyContext.Provider> ); } }
為了防止這種情況,可以將 value 狀態(tài)提升到父節(jié)點(diǎn)的 state 里:
class App extends React.Component { constructor(props) { super(props); this.state = { value: { something: 'something' }, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
五、對(duì)比 useSelector
從「注意事項(xiàng)」可以考慮:要想使消費(fèi)組件進(jìn)行重渲染,context value
必須返回一個(gè)全新對(duì)象,這將導(dǎo)致所有消費(fèi)組件都進(jìn)行重渲染,這個(gè)開(kāi)銷(xiāo)是非常大的,因?yàn)橛幸恍┙M件所依賴的值可能并未發(fā)生變化。
當(dāng)然有一種直觀做法是將「狀態(tài)」分離在不同 Context
之中。
react-redux useSelector
則是采用訂閱 redux store.state
更新,去通知消費(fèi)組件「按需」進(jìn)行重渲染(比較所依賴的 state 前后是否發(fā)生變化)。
- 提供給
Context.Provider
的 value 對(duì)象地址不會(huì)發(fā)生變化,這使得子組件中使用了useSelector -> useContext
,但不會(huì)因頂層數(shù)據(jù)而進(jìn)行重渲染。 -
store.state
數(shù)據(jù)變化組件如何更新呢?react-redux
訂閱了redux store.state
發(fā)生更新的動(dòng)作,然后通知組件「按需」執(zhí)行重渲染。
原文鏈接:https://juejin.cn/post/7169379326649958431
相關(guān)推薦
- 2022-09-15 c語(yǔ)言實(shí)現(xiàn)數(shù)組循環(huán)左移m位_C 語(yǔ)言
- 2022-08-21 Python?正則?re.compile?真的必需嗎_python
- 2022-05-17 Mybatis中報(bào)錯(cuò):attempted to return null from a method
- 2022-07-04 python如何處理matlab的mat數(shù)據(jù)_python
- 2022-05-18 ASP.NET?MVC實(shí)現(xiàn)區(qū)域路由_實(shí)用技巧
- 2023-07-31 elementui使用el-upload組件實(shí)現(xiàn)自定義上傳
- 2022-05-21 云原生自動(dòng)化應(yīng)用于docker倉(cāng)庫(kù)私有憑據(jù)secret創(chuàng)建_docker
- 2022-10-05 redis復(fù)制集群搭建的實(shí)現(xiàn)_Redis
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支