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