Context
本篇我们讲 Context,Context 可以实现跨组件传递数据,大部分的时候并无需要,但有的时候,比如用户设置 了 UI 主题、地区偏好,如果从顶层一层层往下传反而有些麻烦,不如直接借助 Context 实现数据传递。
老的 Context API
基础示例
在讲最新的 API 前,我们先回顾下老的 Context API:
1class Child extends React.Component { 2 render() { 3 // 4. 这里使用 this.context.value 获取 4 return <p>{this.context.value}</p> 5 } 6} 7 8// 3. 子组件添加 contextTypes 静态属性 9Child.contextTypes = { 10 value: PropTypes.string 11}; 12 13class Parent extends React.Component { 14 15 state = { 16 value: 'foo' 17 } 18 19 // 1. 当 state 或者 props 改变的时候,getChildContext 函数就会被调用 20 getChildContext() { 21 return {value: this.state.value} 22 } 23 24 render() { 25 return ( 26 <div> 27 <Child /> 28 </div> 29 ) 30 } 31} 32 33// 2. 父组件添加 childContextTypes 静态属性 34Parent.childContextTypes = { 35 value: PropTypes.string 36};
context 中断问题
对于这个 API,React 官方并不建议使用,对于可能会出现的问题,React 文档给出的介绍为:
问题是,如果组件提供的一个 context 发生了变化,而中间父组件的 shouldComponentUpdate 返回 false,那么使用到该值的后代组件不会进行更新。使用了 context 的组件则完全失控,所以基本上没有办法能够可靠的更新 context。
对于这个问题,我们写个示例代码:
1// 1. Child 组件使用 PureComponent 2class Child extends React.Component { 3 render() { 4 return <GrandChild /> 5 } 6} 7 8class GrandChild extends React.Component { 9 render() { 10 return <p>{this.context.theme}</p> 11 } 12} 13 14GrandChild.contextTypes = { 15 theme: PropTypes.string 16}; 17 18class Parent extends React.Component { 19 20 state = { 21 theme: 'red' 22 } 23 24 getChildContext() { 25 return {theme: this.state.theme} 26 } 27 28 render() { 29 return ( 30 <div onClick={() => { 31 this.setState({ 32 theme: 'blue' 33 }) 34 }}> 35 <Child /> 36 <Child /> 37 </div> 38 ) 39 } 40} 41 42Parent.childContextTypes = { 43 theme: PropTypes.string 44};
在这个示例代码中,当点击文字 red 的时候,文字并不会修改为 blue,如果我们把 Child 改为 extends Component,则能正常修改
这说明当中间组件的 shouldComponentUpdate 为 false 时,会中断 Context 的传递。
PureComponent 的存在是为了减少不必要的渲染,但我们又想 Context 能正常传递,哪有办法可以解决吗?
既然 PureComponent 的存在导致了 Context 无法再更新,那就干脆不更新了,Context 不更新,GrandChild 就无法更新吗?
解决方案
方法当然是有的:
1// 1. 建立一个订阅发布器,当然你也可以称呼它为依赖注入系统(dependency injection system),简称 DI 2class Theme { 3 constructor(value) { 4 this.value = value 5 this.subscriptions = [] 6 } 7 8 setValue(value) { 9 this.value = value 10 this.subscriptions.forEach(f => f()) 11 } 12 13 subscribe(f) { 14 this.subscriptions.push(f) 15 } 16} 17 18 19class Child extends React.PureComponent { 20 render() { 21 return <GrandChild /> 22 } 23} 24 25 26class GrandChild extends React.Component { 27 componentDidMount() { 28 // 4. GrandChild 获取 store 后,进行订阅 29 this.context.theme.subscribe(() => this.forceUpdate()) 30 } 31 32 // 5. GrandChild 从 store 中获取所需要的值 33 render() { 34 return <p>{this.context.theme.value}</p> 35 } 36} 37 38GrandChild.contextTypes = { 39 theme: PropTypes.object 40}; 41 42class Parent extends React.Component { 43 constructor(p, c) { 44 super(p, c) 45 // 2. 我们实例化一个 store(想想 redux 的 store),并存到实例属性中 46 this.theme = new Theme('blue') 47 } 48 49 // 3. 通过 context 传递给 GrandChild 组件 50 getChildContext() { 51 return {theme: this.theme} 52 } 53 54 render() { 55 // 6. 通过 store 进行发布 56 return ( 57 <div onClick={() => { 58 this.theme.setValue('red') 59 }}> 60 <Child /> 61 <Child /> 62 </div> 63 ) 64 } 65} 66 67Parent.childContextTypes = { 68 theme: PropTypes.object 69};
为了管理我们的 theme ,我们建立了一个依赖注入系统(DI),并通过 Context 向下传递 store,需要用到 store 数据的组件进行订阅,传入一个 forceUpdate 函数,当 store 进行发布的时候,依赖 theme 的各个组件执行 forceUpdate,由此实现了在 Context 不更新的情况下实现了各个依赖组件的更新。
你可能也发现了,这有了一点 react-redux 的味道。
当然我们也可以借助 Mobx 来实现并简化代码,具体的实现可以参考 Michel Weststrate(Mobx 的作者) 的 How to safely use React context
新的 Context API
基础示例
想必大家都或多或少的用过,我们直接上示例代码:
1// 1. 创建 Provider 和 Consumer 2const {Provider, Consumer} = React.createContext('dark'); 3 4class Child extends React.Component { 5 // 3. Consumer 组件接收一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。 6 render() { 7 return ( 8 <Consumer> 9 {(theme) => ( 10 <button> 11 {theme} 12 </button> 13 )} 14 </Consumer> 15 ) 16 } 17} 18 19class Parent extends React.Component { 20 21 state = { 22 theme: 'dark', 23 }; 24 25 componentDidMount() { 26 setTimeout(() => { 27 this.setState({ 28 theme: 'light' 29 }) 30 }, 2000) 31 } 32 33 34 render() { 35 // 2. 通过 Provider 的 value 传递值 36 return ( 37 <Provider value={this.state.theme}> 38 <Child /> 39 </Provider> 40 ) 41 } 42}
当 Provider 的 value 值发生变化时,它内部的所有 consumer 组件都会重新渲染。
新 API 的好处就在于从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
模拟实现
那么 createContext 是怎么实现的呢?我们先不看源码,根据前面的订阅发布器的经验,我们自己其实就可以写出一个 createContext 来,我们写一个试试:
1class Store { 2 constructor() { 3 this.subscriptions = [] 4 } 5 6 publish(value) { 7 this.subscriptions.forEach(f => f(value)) 8 } 9 10 subscribe(f) { 11 this.subscriptions.push(f) 12 } 13} 14 15function createContext(defaultValue) { 16 const store = new Store(); 17 18 // Provider 19 class Provider extends React.PureComponent { 20 componentDidUpdate() { 21 store.publish(this.props.value); 22 } 23 24 componentDidMount() { 25 store.publish(this.props.value); 26 } 27 28 render() { 29 return this.props.children; 30 } 31 } 32 33 // Consumer 34 class Consumer extends React.PureComponent { 35 constructor(props) { 36 super(props); 37 this.state = { 38 value: defaultValue 39 }; 40 41 store.subscribe(value => { 42 this.setState({ 43 value 44 }); 45 }); 46 } 47 48 render() { 49 return this.props.children(this.state.value); 50 } 51 } 52 53 return { 54 Provider, 55 Consumer 56 }; 57}
用我们写的 createContext 替换 React.createContext 方法,你会发现,同样可以运行。
它其实跟解决老 Context API 问题的方法是一样的,只不过是做了一层封装。Consumer 组件构建的时候进行订阅,当 Provider 有更新的时候进行发布,这样就跳过了 PureComponent 的限制,实现 Consumer 组件的更新。
createContext 源码
现在我们去看看真的 createContext 源码,源码位置在 packages/react/src/ReactContext.js,简化后的代码如下:
1import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; 2 3export function createContext(defaultValue) { 4 const context = { 5 $$typeof: REACT_CONTEXT_TYPE, 6 // As a workaround to support multiple concurrent renderers, we categorize 7 // some renderers as primary and others as secondary. We only expect 8 // there to be two concurrent renderers at most: React Native (primary) and 9 // Fabric (secondary); React DOM (primary) and React ART (secondary). 10 // Secondary renderers store their context values on separate fields. 11 _currentValue: defaultValue, 12 _currentValue2: defaultValue, 13 // Used to track how many concurrent renderers this context currently 14 // supports within in a single renderer. Such as parallel server rendering. 15 _threadCount: 0, 16 // These are circular 17 Provider: null, 18 Consumer: null, 19 20 // Add these to use same hidden class in VM as ServerContext 21 _defaultValue: null, 22 _globalName: null, 23 }; 24 25 context.Provider = { 26 $$typeof: REACT_PROVIDER_TYPE, 27 _context: context, 28 }; 29 30 context.Consumer = context; 31 32 33 return context; 34}
你会发现,如同之前的文章中涉及的源码一样,React 的 createContext 就只是返回了一个数据对象,但没有关系,以后的文章中会慢慢解析实现过程。
React 系列
讲解 React 源码、React API 背后的实现机制,React 最佳实践、React 的发展与历史等,预计 50 篇左右,欢迎关注
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
