IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    React 之 Context 的变迁与背后实现

    冴羽发表于 2022-12-12 20:46:23
    love 0

    Context

    本篇我们讲 Context,Context 可以实现跨组件传递数据,大部分的时候并无需要,但有的时候,比如用户设置 了 UI 主题、地区偏好,如果从顶层一层层往下传反而有些麻烦,不如直接借助 Context 实现数据传递。

    老的 Context API

    基础示例

    在讲最新的 API 前,我们先回顾下老的 Context API:

    class Child extends React.Component {
      render() {
        // 4. 这里使用 this.context.value 获取
        return <p>{this.context.value}</p>
      }
    }
    
    // 3. 子组件添加 contextTypes 静态属性
    Child.contextTypes = {
      value: PropTypes.string
    };
    
    class Parent extends React.Component {
    
      state = {
        value: 'foo'
      }
    
      // 1. 当 state 或者 props 改变的时候,getChildContext 函数就会被调用
      getChildContext() {
        return {value: this.state.value}
      }
    
      render() {
        return (
          <div>
            <Child />
          </div>
        )
      }
    }
    
    // 2. 父组件添加 childContextTypes 静态属性
    Parent.childContextTypes = {
      value: PropTypes.string
    };

    context 中断问题

    对于这个 API,React 官方并不建议使用,对于可能会出现的问题,React 文档给出的介绍为:

    问题是,如果组件提供的一个 context 发生了变化,而中间父组件的 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 为 false 时,会中断 Context 的传递。

    PureComponent 的存在是为了减少不必要的渲染,但我们又想 Context 能正常传递,哪有办法可以解决吗?

    既然 PureComponent 的存在导致了 Context 无法再更新,那就干脆不更新了,Context 不更新,GrandChild 就无法更新吗?

    解决方案

    方法当然是有的:

    // 1. 建立一个订阅发布器,当然你也可以称呼它为依赖注入系统(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 进行发布
            return (
                <div onClick={() => {
                    this.theme.setValue('red')
                }}>
                  <Child />
                  <Child />
                </div>
            )
        }
    }
    
    Parent.childContextTypes = {
      theme: PropTypes.object
    };

    为了管理我们的 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. 创建 Provider 和 Consumer
    const {Provider, Consumer} = React.createContext('dark');
    
    class Child extends React.Component {
      // 3. Consumer 组件接收一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。
      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 值发生变化时,它内部的所有 consumer 组件都会重新渲染。

    新 API 的好处就在于从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

    模拟实现

    那么 createContext 是怎么实现的呢?我们先不看源码,根据前面的订阅发布器的经验,我们自己其实就可以写出一个 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 方法,你会发现,同样可以运行。

    它其实跟解决老 Context API 问题的方法是一样的,只不过是做了一层封装。Consumer 组件构建的时候进行订阅,当 Provider 有更新的时候进行发布,这样就跳过了 PureComponent 的限制,实现 Consumer 组件的更新。

    createContext 源码

    现在我们去看看真的 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;
    }

    你会发现,如同之前的文章中涉及的源码一样,React 的 createContext 就只是返回了一个数据对象,但没有关系,以后的文章中会慢慢解析实现过程。

    React 系列

    讲解 React 源码、React API 背后的实现机制,React 最佳实践、React 的发展与历史等,预计 50 篇左右,欢迎关注

    如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。



沪ICP备19023445号-2号
友情链接