一文帶你深入理解React中的Context
前言
React Context是React提供給開發(fā)者的一種常用的狀態(tài)管理機(jī)制,利用Context可以有效的將同一個狀態(tài)在多級組件中進(jìn)行傳遞,并能夠在狀態(tài)更新時,自動的通知各個組件進(jìn)行更新。那React Context又是如何做到這一點(diǎn)的,以及為什么需要這么設(shè)計呢?
為什么需要Context
在React的數(shù)據(jù)管理理念中,一直遵循著單項(xiàng)數(shù)據(jù)流以及數(shù)據(jù)不變性的理念。當(dāng)我們需要從父組件將狀態(tài)向子組件傳遞時,我們往往需要通過Props顯式進(jìn)行傳遞,例如:
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Son count={count} />
)
}
const Son:FC = (props) => {
const { count } = props;
return (
<span>{count}</span>
)
}但是,倘若父組件需要向子組件的子組件,也就是孫組件進(jìn)行狀態(tài)的傳遞呢?或者父組件需要同時向多個子組件進(jìn)行傳遞呢?當(dāng)然,繼續(xù)使用props進(jìn)行逐層往下的顯示傳遞肯定也是能實(shí)現(xiàn)這個需求的,但那樣的代碼未免過于繁瑣且難以維護(hù),如果能夠在父組件里維護(hù)一個類似于Js里的全局變量,所有的子組件都能使用這個全局變量不就好了嗎?
是的,這個就是Context的作用,但又遠(yuǎn)遠(yuǎn)不止這么簡單。
Context是什么
Context提供了一個無需為每層組件手動添加 props,就能在組件樹間進(jìn)行數(shù)據(jù)傳遞的方法。

Context如何使用
創(chuàng)建Context
首先,我們需要在父組件中, 利用React.createContext創(chuàng)建一個React Context對象,這個方法接受一個入?yún)ⅲ鳛楫?dāng)前Context的默認(rèn)值。
import React from 'react' const Context = React.createContext(defaultValue)
向下傳遞數(shù)據(jù)
利用Context對象返回的Provide組件,包裹需要傳遞數(shù)據(jù)的子組件。
每個 Context 對象都會返回一個 Provider React 組件,它接收一個 value 屬性,可將數(shù)據(jù)向下傳遞給消費(fèi)組件。當(dāng) Provider 的 value 值發(fā)生變化時,它內(nèi)部的所有消費(fèi)組件都會重新渲染。
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Context.Provider value={count}>
<Son />
</Context.Provider>
)
}接收數(shù)據(jù)
被包裹的子組件,利用useContext獲取父組件傳遞的數(shù)據(jù)。
const Son:FC = (props) => {
const value = React.useContext(Context);
return (
<span>{value}</span>
)
}Context如何以及為何這樣實(shí)現(xiàn)
讓我們回到Context使用過程的第一步,通過閱讀源碼去研究createContext究竟做了什么樣的工作?
剔除了一些干擾代碼,其實(shí)createContext做的事情其實(shí)非常簡單,創(chuàng)建了一個對象,保存了當(dāng)前context的value, 以及返回了一個Provide組件。
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactProviderType} from 'shared/ReactTypes';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
// TODO: Second argument used to be an optional `calculateChangedBits`
// function. Warn to reserve for future use?
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_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),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}在React編譯的過程中,會將我們寫的JSX語法代碼,轉(zhuǎn)化成React.createElement方法,執(zhí)行這個方法后,會得到一個ReactElement元素對象,也就是我們所說的Virtual Dom。這個元素對象,會記錄著當(dāng)前組件所接收的入?yún)⒁约霸仡愋汀?/p>
而Provide組件實(shí)際上編譯完之后也是一個ReactElement,只不過他的Type跟正常的組件并不一樣,而是context.Provider。
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};那么,子組件又是如何利用Provider和useContext去獲取到最新的數(shù)據(jù)的呢?
useContext接收一個context對象作為參數(shù),從context._currentValue中讀取當(dāng)前contetx的value值。
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
// 獲取當(dāng)前context保存的value
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// ...do something
// 返回當(dāng)前的值
return value;
}問題又來了,當(dāng)父組件的狀態(tài)改變時,又是如何通過Provider觸發(fā)更新,通知訂閱當(dāng)前狀態(tài)的子組件進(jìn)行重新渲染的呢?
當(dāng)父組件的狀態(tài)進(jìn)行更新時,React整體會進(jìn)入到調(diào)度更新階段,F(xiàn)iber節(jié)點(diǎn)會進(jìn)入到beginWork的方法當(dāng)中,在這個方法里面,會根據(jù)當(dāng)前更新節(jié)點(diǎn)的類型,從而執(zhí)行相對應(yīng)的方法。上文提到,Provider組件是有單獨(dú)的自己的類型ContextProvider的,所以會進(jìn)入到相對應(yīng)的更新方法,updateContextProvide。
其實(shí)updateContextProvide里做的事情,大抵可以概括為:
首先更新context._currentValue, 然后比較新老value是否發(fā)生改變,如果沒有發(fā)生改變,則跳出更新函數(shù),復(fù)用當(dāng)前fiber節(jié)點(diǎn)。如果發(fā)生了改變,則調(diào)用一個叫propagateContextChange的方法,對該P(yáng)rovider組件的子組件進(jìn)行深度遍歷,找到訂閱了當(dāng)前context的子組件,并打上需要更新的標(biāo)記,lane。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
if (enableLazyContextPropagation) {
// In the lazy propagation implementation, we don't scan for matching
// consumers until something bails out, because until something bails out
// we're going to visit those nodes, anyway. The trade-off is that it shifts
// responsibility to the consumer to track whether something has changed.
} else {
if (oldProps !== null) {
const oldValue = oldProps.value;
if (is(oldValue, newValue)) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
// do something...
}那么, 在深度遍歷的時候,又是如何知道當(dāng)前子組件是否有訂閱當(dāng)前Context的呢?
其實(shí)在使用useContext的時候,除了讀取當(dāng)前context的value,還會把接收的context對象信息保存在當(dāng)前組件的Fiber.dependencies上,所以在遍歷的時候,只需要看當(dāng)前組件的dependencies上有沒有當(dāng)前context便可以知道當(dāng)前組件是否存在訂閱關(guān)系了。
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}只有被Provider組件包裹的子組件才能讀取到Context的狀態(tài)嗎?
其實(shí)并不是,所有的組件都可以通過useContext去讀取Context對象里的currentValue,但是,只有被Provider組件包裹的組件,才能訂閱到Context對象里的value的變化,在變化的時候及時的更新自身組件的狀態(tài)。這樣設(shè)計的目的,實(shí)際上也是為了更好的優(yōu)化React在更新組件的性能,試想,如果每創(chuàng)建一個Context對象,就默認(rèn)所有的組件都可以訂閱到這個Context的變化,那么整個Fiber樹在更新的過程中,需要遍歷的Fiber節(jié)點(diǎn)就太龐大了,一些完全不需要且沒有訂閱當(dāng)前Context的組件也需要被遍歷到,這其實(shí)是一種性能的浪費(fèi)。
到此這篇關(guān)于一文帶你深入理解React中的Context的文章就介紹到這了,更多相關(guān)React Context內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React創(chuàng)建對話框組件的方法實(shí)例
在項(xiàng)目開發(fā)過程中,對于復(fù)雜的業(yè)務(wù)選擇功能很常見,下面這篇文章主要給大家介紹了關(guān)于React創(chuàng)建對話框組件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05
詳解React Angular Vue三大前端技術(shù)
當(dāng)前世界中,技術(shù)發(fā)展非常迅速并且變化迅速,開發(fā)者需要更多的開發(fā)工具來解決不同的問題。本文就對于當(dāng)下主流的前端開發(fā)技術(shù)React、Vue、Angular這三個框架做個相對詳盡的探究,目的是為了解開這些前端技術(shù)的面紗,看看各自的廬山真面目。2021-05-05
React Native項(xiàng)目框架搭建的一些心得體會
React Native使你能夠在Javascript和React的基礎(chǔ)上獲得完全一致的開發(fā)體驗(yàn),構(gòu)建世界一流的原生APP。接下來通過本文給大家分享React Native項(xiàng)目框架搭建的一些心得體會,感興趣的朋友跟隨小編一起看看吧2021-05-05

