一文帶你深入理解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)不止這么簡(jiǎ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í)非常簡(jiǎn)單,創(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創(chuàng)建對(duì)話框組件的方法實(shí)例
在項(xiàng)目開發(fā)過程中,對(duì)于復(fù)雜的業(yè)務(wù)選擇功能很常見,下面這篇文章主要給大家介紹了關(guān)于React創(chuàng)建對(duì)話框組件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05詳解React Angular Vue三大前端技術(shù)
當(dāng)前世界中,技術(shù)發(fā)展非常迅速并且變化迅速,開發(fā)者需要更多的開發(fā)工具來解決不同的問題。本文就對(duì)于當(dāng)下主流的前端開發(fā)技術(shù)React、Vue、Angular這三個(gè)框架做個(gè)相對(duì)詳盡的探究,目的是為了解開這些前端技術(shù)的面紗,看看各自的廬山真面目。2021-05-05React Native項(xiàng)目框架搭建的一些心得體會(huì)
React Native使你能夠在Javascript和React的基礎(chǔ)上獲得完全一致的開發(fā)體驗(yàn),構(gòu)建世界一流的原生APP。接下來通過本文給大家分享React Native項(xiàng)目框架搭建的一些心得體會(huì),感興趣的朋友跟隨小編一起看看吧2021-05-05