React?Context?變遷及背后實(shí)現(xiàn)原理詳解
Context
本篇我們講 Context,Context 可以實(shí)現(xiàn)跨組件傳遞數(shù)據(jù),大部分的時(shí)候并無需要,但有的時(shí)候,比如用戶設(shè)置 了 UI 主題、地區(qū)偏好,如果從頂層一層層往下傳反而有些麻煩,不如直接借助 Context 實(shí)現(xiàn)數(shù)據(jù)傳遞。
老的 Context API
基礎(chǔ)示例
在講最新的 API 前,我們先回顧下老的 Context API:
class Child extends React.Component {
render() {
// 4. 這里使用 this.context.value 獲取
return <p>{this.context.value}</p>
}
}
// 3. 子組件添加 contextTypes 靜態(tài)屬性
Child.contextTypes = {
value: PropTypes.string
};
class Parent extends React.Component {
state = {
value: 'foo'
}
// 1. 當(dāng) state 或者 props 改變的時(shí)候,getChildContext 函數(shù)就會(huì)被調(diào)用
getChildContext() {
return {value: this.state.value}
}
render() {
return (
<div>
<Child />
</div>
)
}
}
// 2. 父組件添加 childContextTypes 靜態(tài)屬性
Parent.childContextTypes = {
value: PropTypes.string
};
context 中斷問題
對(duì)于這個(gè) API,React 官方并不建議使用,對(duì)于可能會(huì)出現(xiàn)的問題,React 文檔給出的介紹為:
問題是,如果組件提供的一個(gè) context 發(fā)生了變化,而中間父組件的 shouldComponentUpdate 返回 false,那么使用到該值的后代組件不會(huì)進(jìn)行更新。使用了 context 的組件則完全失控,所以基本上沒有辦法能夠可靠的更新 context。
對(duì)于這個(gè)問題,我們寫個(gè)示例代碼:
// 1. Child 組件使用 PureComponent
class Child extends React.Component {
render() {
return <GrandChild />
}
}
class GrandChild extends React.Component {
render() {
return <p>{this.context.theme}</p>
}
}
GrandChild.contextTypes = {
theme: PropTypes.string
};
class Parent extends React.Component {
state = {
theme: 'red'
}
getChildContext() {
return {theme: this.state.theme}
}
render() {
return (
<div onClick={() => {
this.setState({
theme: 'blue'
})
}}>
<Child />
<Child />
</div>
)
}
}
Parent.childContextTypes = {
theme: PropTypes.string
};
在這個(gè)示例代碼中,當(dāng)點(diǎn)擊文字 red 的時(shí)候,文字并不會(huì)修改為 blue,如果我們把 Child 改為 extends Component,則能正常修改
這說明當(dāng)中間組件的 shouldComponentUpdate 為 true 時(shí),會(huì)中斷 Context 的傳遞。
PureComponent 的存在是為了減少不必要的渲染,但我們又想 Context 能正常傳遞,哪有辦法可以解決嗎?
既然 PureComponent 的存在導(dǎo)致了 Context 無法再更新,那就干脆不更新了,Context 不更新,GrandChild 就無法更新嗎?
解決方案
方法當(dāng)然是有的:
// 1. 建立一個(gè)訂閱發(fā)布器,當(dāng)然你也可以稱呼它為依賴注入系統(tǒng)(dependency injection system),簡稱 DI
class Theme {
constructor(value) {
this.value = value
this.subscriptions = []
}
setValue(value) {
this.value = value
this.subscriptions.forEach(f => f())
}
subscribe(f) {
this.subscriptions.push(f)
}
}
class Child extends React.PureComponent {
render() {
return <GrandChild />
}
}
class GrandChild extends React.Component {
componentDidMount() {
// 4. GrandChild 獲取 store 后,進(jìn)行訂閱
this.context.theme.subscribe(() => this.forceUpdate())
}
// 5. GrandChild 從 store 中獲取所需要的值
render() {
return <p>{this.context.theme.value}</p>
}
}
GrandChild.contextTypes = {
theme: PropTypes.object
};
class Parent extends React.Component {
constructor(p, c) {
super(p, c)
// 2. 我們實(shí)例化一個(gè) store(想想 redux 的 store),并存到實(shí)例屬性中
this.theme = new Theme('blue')
}
// 3. 通過 context 傳遞給 GrandChild 組件
getChildContext() {
return {theme: this.theme}
}
render() {
// 6. 通過 store 進(jìn)行發(fā)布
return (
<div onClick={() => {
this.theme.setValue('red')
}}>
<Child />
<Child />
</div>
)
}
}
Parent.childContextTypes = {
theme: PropTypes.object
};
為了管理我們的 theme ,我們建立了一個(gè)依賴注入系統(tǒng)(DI),并通過 Context 向下傳遞 store,需要用到 store 數(shù)據(jù)的組件進(jìn)行訂閱,傳入一個(gè) forceUpdate 函數(shù),當(dāng) store 進(jìn)行發(fā)布的時(shí)候,依賴 theme 的各個(gè)組件執(zhí)行 forceUpdate,由此實(shí)現(xiàn)了在 Context 不更新的情況下實(shí)現(xiàn)了各個(gè)依賴組件的更新。
你可能也發(fā)現(xiàn)了,這有了一點(diǎn) react-redux 的味道。
當(dāng)然我們也可以借助 Mobx 來實(shí)現(xiàn)并簡化代碼,具體的實(shí)現(xiàn)可以參考 Michel Weststrate(Mobx 的作者) 的 How to safely use React context
新的 Context API
基礎(chǔ)示例
想必大家都或多或少的用過,我們直接上示例代碼:
// 1. 創(chuàng)建 Provider 和 Consumer
const {Provider, Consumer} = React.createContext('dark');
class Child extends React.Component {
// 3. Consumer 組件接收一個(gè)函數(shù)作為子元素。這個(gè)函數(shù)接收當(dāng)前的 context 值,并返回一個(gè) React 節(jié)點(diǎn)。
render() {
return (
<Consumer>
{(theme) => (
<button>
{theme}
</button>
)}
</Consumer>
)
}
}
class Parent extends React.Component {
state = {
theme: 'dark',
};
componentDidMount() {
setTimeout(() => {
this.setState({
theme: 'light'
})
}, 2000)
}
render() {
// 2. 通過 Provider 的 value 傳遞值
return (
<Provider value={this.state.theme}>
<Child />
</Provider>
)
}
}
當(dāng) Provider 的 value 值發(fā)生變化時(shí),它內(nèi)部的所有 consumer 組件都會(huì)重新渲染。
新 API 的好處就在于從 Provider 到其內(nèi)部 consumer 組件(包括 .contextType 和 useContext)的傳播不受制于 shouldComponentUpdate 函數(shù),因此當(dāng) consumer 組件在其祖先組件跳過更新的情況下也能更新。
模擬實(shí)現(xiàn)
那么 createContext 是怎么實(shí)現(xiàn)的呢?我們先不看源碼,根據(jù)前面的訂閱發(fā)布器的經(jīng)驗(yàn),我們自己其實(shí)就可以寫出一個(gè) createContext 來,我們寫一個(gè)試試:
class Store {
constructor() {
this.subscriptions = []
}
publish(value) {
this.subscriptions.forEach(f => f(value))
}
subscribe(f) {
this.subscriptions.push(f)
}
}
function createContext(defaultValue) {
const store = new Store();
// Provider
class Provider extends React.PureComponent {
componentDidUpdate() {
store.publish(this.props.value);
}
componentDidMount() {
store.publish(this.props.value);
}
render() {
return this.props.children;
}
}
// Consumer
class Consumer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
value: defaultValue
};
store.subscribe(value => {
this.setState({
value
});
});
}
render() {
return this.props.children(this.state.value);
}
}
return {
Provider,
Consumer
};
}
用我們寫的 createContext 替換 React.createContext 方法,你會(huì)發(fā)現(xiàn),同樣可以運(yùn)行。
它其實(shí)跟解決老 Context API 問題的方法是一樣的,只不過是做了一層封裝。Consumer 組件構(gòu)建的時(shí)候進(jìn)行訂閱,當(dāng) Provider 有更新的時(shí)候進(jìn)行發(fā)布,這樣就跳過了 PureComponent 的限制,實(shí)現(xiàn) Consumer 組件的更新。
createContext 源碼
現(xiàn)在我們?nèi)タ纯凑娴?createContext 源碼,源碼位置在 packages/react/src/ReactContext.js,簡化后的代碼如下:
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
export function createContext(defaultValue) {
const context = {
$$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,
Consumer: null,
// Add these to use same hidden class in VM as ServerContext
_defaultValue: null,
_globalName: null,
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
return context;
}
你會(huì)發(fā)現(xiàn),如同之前的文章中涉及的源碼一樣,React 的 createContext 就只是返回了一個(gè)數(shù)據(jù)對(duì)象,但沒有關(guān)系,以后的文章中會(huì)慢慢解析實(shí)現(xiàn)過程。
React 系列
React 之 Refs 的使用和 forwardRef 的源碼解讀
React 系列的預(yù)熱系列,帶大家從源碼的角度深入理解 React 的
以上就是React Context 變遷及背后實(shí)現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于React Context 變遷原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React組件的創(chuàng)建與state同步異步詳解
這篇文章主要介紹了react組件實(shí)例屬性state,有狀態(tài)state的組件稱作復(fù)雜組件,沒有狀態(tài)的組件稱為簡單組件,狀態(tài)里存儲(chǔ)數(shù)據(jù),數(shù)據(jù)的改變驅(qū)動(dòng)頁面的展示,本文結(jié)合實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2023-03-03
react-router實(shí)現(xiàn)跳轉(zhuǎn)傳值的方法示例
這篇文章主要給大家介紹了關(guān)于react-router實(shí)現(xiàn)跳轉(zhuǎn)傳值的相關(guān)資料,文中給出了詳細(xì)的示例代碼,對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-05-05

