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

Context如何使用
創(chuàng)建Context
首先,我們需要在父組件中, 利用React.createContext創(chuàng)建一個(gè)React Context對(duì)象,這個(gè)方法接受一個(gè)入?yún)?,作為?dāng)前Context的默認(rèn)值。
import React from 'react' const Context = React.createContext(defaultValue)
向下傳遞數(shù)據(jù)
利用Context對(duì)象返回的Provide組件,包裹需要傳遞數(shù)據(jù)的子組件。
每個(gè) Context 對(duì)象都會(huì)返回一個(gè) Provider React 組件,它接收一個(gè) value 屬性,可將數(shù)據(jù)向下傳遞給消費(fèi)組件。當(dāng) Provider 的 value 值發(fā)生變化時(shí),它內(nèi)部的所有消費(fèi)組件都會(huì)重新渲染。
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)建了一個(gè)對(duì)象,保存了當(dāng)前context的value, 以及返回了一個(gè)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編譯的過程中,會(huì)將我們寫的JSX語法代碼,轉(zhuǎn)化成React.createElement方法,執(zhí)行這個(gè)方法后,會(huì)得到一個(gè)ReactElement元素對(duì)象,也就是我們所說的Virtual Dom。這個(gè)元素對(duì)象,會(huì)記錄著當(dāng)前組件所接收的入?yún)⒁约霸仡愋汀?/p>
而Provide組件實(shí)際上編譯完之后也是一個(gè)ReactElement,只不過他的Type跟正常的組件并不一樣,而是context.Provider。
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};那么,子組件又是如何利用Provider和useContext去獲取到最新的數(shù)據(jù)的呢?
useContext接收一個(gè)context對(duì)象作為參數(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)改變時(shí),又是如何通過Provider觸發(fā)更新,通知訂閱當(dāng)前狀態(tài)的子組件進(jìn)行重新渲染的呢?
當(dāng)父組件的狀態(tài)進(jìn)行更新時(shí),React整體會(huì)進(jìn)入到調(diào)度更新階段,F(xiàn)iber節(jié)點(diǎn)會(huì)進(jìn)入到beginWork的方法當(dāng)中,在這個(gè)方法里面,會(huì)根據(jù)當(dāng)前更新節(jié)點(diǎn)的類型,從而執(zhí)行相對(duì)應(yīng)的方法。上文提到,Provider組件是有單獨(dú)的自己的類型ContextProvider的,所以會(huì)進(jìn)入到相對(duì)應(yīng)的更新方法,updateContextProvide。
其實(shí)updateContextProvide里做的事情,大抵可以概括為:
首先更新context._currentValue, 然后比較新老value是否發(fā)生改變,如果沒有發(fā)生改變,則跳出更新函數(shù),復(fù)用當(dāng)前fiber節(jié)點(diǎn)。如果發(fā)生了改變,則調(diào)用一個(gè)叫propagateContextChange的方法,對(duì)該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...
}那么, 在深度遍歷的時(shí)候,又是如何知道當(dāng)前子組件是否有訂閱當(dāng)前Context的呢?
其實(shí)在使用useContext的時(shí)候,除了讀取當(dāng)前context的value,還會(huì)把接收的context對(duì)象信息保存在當(dāng)前組件的Fiber.dependencies上,所以在遍歷的時(shí)候,只需要看當(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對(duì)象里的currentValue,但是,只有被Provider組件包裹的組件,才能訂閱到Context對(duì)象里的value的變化,在變化的時(shí)候及時(shí)的更新自身組件的狀態(tài)。這樣設(shè)計(jì)的目的,實(shí)際上也是為了更好的優(yōu)化React在更新組件的性能,試想,如果每創(chuàng)建一個(gè)Context對(duì)象,就默認(rèn)所有的組件都可以訂閱到這個(gè)Context的變化,那么整個(gè)Fiber樹在更新的過程中,需要遍歷的Fiber節(jié)點(diǎn)就太龐大了,一些完全不需要且沒有訂閱當(dāng)前Context的組件也需要被遍歷到,這其實(shí)是一種性能的浪費(fèi)。
到此這篇關(guān)于一文帶你深入理解React中的Context的文章就介紹到這了,更多相關(guān)React Context內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解react native頁面間傳遞數(shù)據(jù)的幾種方式
這篇文章主要介紹了詳解react native頁面間傳遞數(shù)據(jù)的幾種方式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11
react-router?v6實(shí)現(xiàn)權(quán)限管理+自動(dòng)替換頁面標(biāo)題的案例
這篇文章主要介紹了react-router?v6實(shí)現(xiàn)權(quán)限管理+自動(dòng)替換頁面標(biāo)題,這次項(xiàng)目是有三種權(quán)限,分別是用戶,商家以及管理員,這次寫的權(quán)限管理是高級(jí)權(quán)限能訪問低級(jí)權(quán)限的所有頁面,但是低級(jí)權(quán)限不能訪問高級(jí)權(quán)限的頁面,需要的朋友可以參考下2023-05-05
Electron整合React使用搭建開發(fā)環(huán)境的步驟詳解
這篇文章主要介紹了Electron整合React使用搭建開發(fā)環(huán)境,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2020-06-06
react router4+redux實(shí)現(xiàn)路由權(quán)限控制的方法
本篇文章主要介紹了react router4+redux實(shí)現(xiàn)路由權(quán)限控制的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
快速創(chuàng)建React項(xiàng)目并配置webpack
這篇文章主要介紹了創(chuàng)建React項(xiàng)目并配置webpack,在這里需要注意,Create?React?App?requires?Node?14?or?higher.需要安裝高版本的node,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-01-01
React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放時(shí)間滾動(dòng))
這篇文章主要為大家詳細(xì)介紹了React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放使勁按滾動(dòng)),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2024-02-02

