mobx-react之Provider和inject的原理与实现

5,248 阅读9分钟

Provider 与 inject

Provider 组件利用 React 的 Context 上下文机制在组件间跨层级传递数据,它既支持传递 store,也支持传递其它非 store 数据。比如我们不想在多层级组件间层层传递数据,就可以使用 Provider 组件将数据传递给子孙组件。

inject 在子孙组件中注入 Provider 传递的 store,其本质上是一个高阶组件。

Provider 和 inject 的使用

创建 store

//  /store/todoStore.js

import {observable, configure, action, computed, autorun} from "mobx";

// 配置enforceActions 为 observed,表示不允许在动作外部修改状态
configure({enforceActions: "observed"});

class TodoStore {
  // 使用 @observable 装饰器将 todos 变为一个可观察对象
  @observable todos = [
    {
      id: "0",
      // 标记任务是否完成
      finished: false,
      // 定义任务名
      title: "任务1"
    },
    {
      id: "1",
      // 标记任务是否完成
      finished: true,
      // 定义任务名
      title: "任务2"
    },
    {
      id: "2",
      // 标记任务是否完成
      finished: false,
      // 定义任务名
      title: "任务3"
    }
  ];

  @computed get unfinishedCount() {
    return this.todos.filter(todo => !todo.finished).length;
  }

    // @action 定义一个动作,修改状态
  @action change(todo) {
    todo.finished = !todo.finished;
  }
}

const todoStore = new TodoStore();

autorun(() => {
  console.log("剩余任务:" + todoStore.unfinishedCount + "个"); //sy-log
});

export default todoStore;

导出store

在 mobx 中,我们可以创建多个 store,为了方便管理,我们在一个文件中将 store 统一导出:

// /store/index.js

import TodoStore from './todoStore';

export const todoStore = TodoStore;

注入store

然后在根组件中通过Provider组件注入store:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
import { Provider } from 'mobx-react';
import {todoStore} from "./store/index";

ReactDOM.render(
  <Provider todoStore={todoStore} omg="omg-omg">
    <App />
  </Provider>,
  document.getElementById("root")
);

获取store

在子组件中我们通过 inject 获取 Provider 传递的 store 。

inject 给函数组件注入数据:

import React, {Component, Children, useReducer} from "react";
import {observer as observerLite, Observer, useObserver} from "mobx-react-lite";
import {observer, inject, MobXProviderContext} from "mobx-react";

// 使用inject给函数组件注入数据
const TodoList = inject(
  "todoStore",
  "omg"
)(props => {
  return (
    <div>
      <h3>TodoList</h3>
      {props.todoStore.todos.map(todo => (
        <Todo key={todo.id} todo={todo} change={props.todoStore.change} />
      ))}
      <Observer>
          {
            () => (<p>未完成任务: {props.todoStore.unfinishedCount}个</p>)
          }
      </Observer>
      <p>{props.omg}</p>
    </div>
  );
});


export default TodoList;

// 使用 observer 给组件添加响应式 
const Todo = observer(
  ({todo, change}, ref) => {
    console.log("input value", ref.current && ref.current.value); //sy-log
    return (
      <div>
        <input
        id={todo.title}
        type="checkbox"
        checked={todo.finished}
        onChange={() => change(todo)}
        />
        <label for={todo.title}>{todo.title}</label>
      </div>
    );
  },
  {forwardRef: true}
);

inject 给类组件注入数据:

import React, {Component, Children, useReducer} from "react";
import {observer as observerLite, Observer, useObserver} from "mobx-react-lite";
import {observer, inject, MobXProviderContext} from "mobx-react";

@inject("todoStore", "omg")
@observer
class TodoList extends Component {
  inputRef = React.createRef();
  render() {
    return (
      <div>
        <h3>TodoList</h3>
        <input type="text" ref={this.inputRef} />
        {this.props.todoStore.todos.map(todo => (
          <Todo
            key={todo.id}
            todo={todo}
            change={this.props.todoStore.change}
            ref={this.inputRef}
          />
        ))}
        <p>未完成任务: {this.props.todoStore.unfinishedCount}个</p>
        <p>{this.props.omg}</p>
      </div>
    );
  }
}

export default TodoList;

// 给组件添加响应式 observer
const Todo = observer(
  ({todo, change}, ref) => {
    console.log("input value", ref.current && ref.current.value); //sy-log
    return (
      <div>
        <input
        id={todo.title}
        type="checkbox"
        checked={todo.finished}
        onChange={() => change(todo)}
        />
        <label for={todo.title}>{todo.title}</label>
      </div>
    );
  },
  {forwardRef: true}
);

在子孙组件中,除了使用 inject 获取 Provider 传递的 store,还可以使用 Cosumer 组件和 React.useContext hook 获取 Provider 传递的 store。

使用 Cosumer 组件获取数据

// 通过 Consumer 组件获取数据
class TodoList extends Component {
  inputRef = React.createRef();
  render() {
    return (
      <MobXProviderContext.Consumer>
        {({todoStore, omg}) => {
          return (
            <Observer>
              {() => (
                <div>
                  <h3>TodoList</h3>
                  <input type="text" ref={this.inputRef} />
                  {todoStore.todos.map(todo => (
                    <Todo
                      key={todo.id}
                      todo={todo}
                      change={todoStore.change}
                      ref={this.inputRef}
                    />
                  ))}
                  <p>未完成任务: {todoStore.unfinishedCount}个</p>
                  <p>{omg}</p>
                </div>
              )}
            </Observer>
          );
        }}
      </MobXProviderContext.Consumer>
    );
  }
}

export default TodoList;

// 自己配置forwardRef
const Todo = observer(
  React.forwardRef((props, ref) => {
    const {todo, change} = props;
    // console.log("input value", ref.current && ref.current.value); //sy-log
    return (
      <div>
        <input
          type="checkbox"
          checked={todo.finished}
          onChange={() => change(todo)}
        />
        {todo.title}
      </div>
    );
  })
);

使用 React.useContext hook 获取数据

// 使用useContext
const TodoList = observer(props => {
  const {todoStore, omg} = React.useContext(MobXProviderContext);
  return (
    <div>
      <h3>TodoList</h3>
      {todoStore.todos.map(todo => (
        <Todo key={todo.id} todo={todo} change={todoStore.change} />
      ))}
      <p>未完成任务: {todoStore.unfinishedCount}个</p>
      <p>{omg}</p>
    </div>
  );
});

export default TodoList;

// 给组件添加响应式 observer
const Todo = observer(
  ({todo, change}, ref) => {
    console.log("input value", ref.current && ref.current.value); //sy-log
    return (
      <div>
        <input
        id={todo.title}
        type="checkbox"
        checked={todo.finished}
        onChange={() => change(todo)}
        />
        <label for={todo.title}>{todo.title}</label>
      </div>
    );
  },
  {forwardRef: true}
);

Provider 与 inject 源码分析

我们首先来看看 Provider 的源码:

Provider 源码分析

import React from "react"
import { shallowEqual } from "./utils/utils"
import { IValueMap } from "./types/IValueMap"

// 创建一个 React Context 上下文对象
export const MobXProviderContext = React.createContext<IValueMap>({})

export interface ProviderProps extends IValueMap {
    children: React.ReactNode
}

export function Provider(props: ProviderProps) {
    const { children, ...stores } = props
    // 使用 useContext hook 获取当前 Context 上下文对象的当前值
    const parentValue = React.useContext(MobXProviderContext)
    // 使用 useRef hook 创建一个可变的 ref 对象,它的 .current 属性的初始值为当前的 Context 上下文对象的当前值和 stores
    const mutableProviderRef = React.useRef({ ...parentValue, ...stores })
    // 通过 ref 对象的 current 属性获取保存在 ref 对象中的 Context 上下文对象当前值和 stores
    const value = mutableProviderRef.current

    if (__DEV__) {
        const newValue = { ...value, ...stores } // spread in previous state for the context based stores
        if (!shallowEqual(value, newValue)) {
            throw new Error(
                "MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error."
            )
        }
    }
        // 将存储在可变对象ref中的  Context 上下文对象的当前值和 stores 传递给 Context 上下文对象的 value属性
      // 当 value 值发生改变时,Provider 内部的子组件都会重新渲染
    return <MobXProviderContext.Provider value={value}>{children}</MobXProviderContext.Provider>
}

Provider.displayName = "MobXProvider"

在 Provider 源码中:

  1. 首先使用 useContext hook 创建一个 React Context上下文对象,以便于在 Provider 组件获取 Context 上下文对象的当前值 和 React Context 对象的 Provider React 组件;

  2. 然后创建一个名为 Provider的函数组件,在函数组件中获取Context 上下文对象的当前值,将当前值和 stores 保存在可变的 ref 对象中

  3. 最后将存储在可变对象ref 中的 Context对象的当前值和 stores 传递给 Context对象的 Provider React 组件的value属性,当Provider的value 属性发生变化时,Provider内部的子组件也会跟着重新渲染

inject 源码分析

import React from "react"
import { observer } from "./observer"
import { copyStaticProperties } from "./utils/utils"
import { MobXProviderContext } from "./Provider"
import { IReactComponent } from "./types/IReactComponent"
import { IValueMap } from "./types/IValueMap"
import { IWrappedComponent } from "./types/IWrappedComponent"
import { IStoresToProps } from "./types/IStoresToProps"

/**
 * Store Injection
 */
function createStoreInjector(
    grabStoresFn: IStoresToProps,
    component: IReactComponent<any>,
    injectNames: string,
    makeReactive: boolean
): IReactComponent<any> {
    // Support forward refs
    let Injector: IReactComponent<any> = React.forwardRef((props, ref) => {
        const newProps = { ...props }
        const context = React.useContext(MobXProviderContext)
        // 合并 props
        // grabStoresFn: 执行用户传递进来的返回新props的函数
        Object.assign(newProps, grabStoresFn(context || {}, newProps) || {})

      // 将 ref 对象添加到新组件的 props 上
        if (ref) {
            newProps.ref = ref
        }
                
        // 根据传进来的组件和 props 创建一个新的组件
        return React.createElement(component, newProps)
    })

    // inject 的第一个参数是函数时,将 Injector 变成响应式组件
    if (makeReactive) Injector = observer(Injector)
    Injector["isMobxInjector"] = true // assigned late to suppress observer warning

    // Static fields from component should be visible on the generated Injector
    copyStaticProperties(component, Injector)
    Injector["wrappedComponent"] = component
        // 给 Injector 组件添加 组件的显示名称
    Injector.displayName = getInjectName(component, injectNames)
    return Injector
}

function getInjectName(component: IReactComponent<any>, injectNames: string): string {
    let displayName
    const componentName =
        component.displayName ||
        component.name ||
        (component.constructor && component.constructor.name) ||
        "Component"
    if (injectNames) displayName = "inject-with-" + injectNames + "(" + componentName + ")"
    else displayName = "inject(" + componentName + ")"
    return displayName
}

function grabStoresByName(
    storeNames: Array<string>
): (baseStores: IValueMap, nextProps: React.Props<any>) => React.PropsWithRef<any> | undefined {
    return function(baseStores, nextProps) {
        storeNames.forEach(function(storeName) {
            if (
                storeName in nextProps // prefer props over stores
            )
                return
            if (!(storeName in baseStores))
                throw new Error(
                    "MobX injector: Store '" +
                        storeName +
                        "' is not available! Make sure it is provided by some Provider"
                )
            nextProps[storeName] = baseStores[storeName]
        })
        return nextProps
    }
}


/**
* 为 inject 函数提供多个函数类型定义实现函数重载
*/

// inject 的参数为 store 时的函数类型定义
export function inject(
    ...stores: Array<string>
): <T extends IReactComponent<any>>(
    target: T
) => T & (T extends IReactComponent<infer P> ? IWrappedComponent<P> : never)

// inject 的参数为function 时的函数类型定义
export function inject<S, P, I, C>(
    fn: IStoresToProps<S, P, I, C>
): <T extends IReactComponent>(target: T) => T & IWrappedComponent<P>

/**
 * higher order component that injects stores to a child.
 * takes either a varargs list of strings, which are stores read from the context,
 * or a function that manually maps the available stores from the context to props:
 * storesToProps(mobxStores, props, context) => newProps
 */
 // inject 的参数可以是一个返回新 props 的函数,也可以是多个 store
 //
export function inject(/* fn(stores, nextProps) or ...storeNames */ ...storeNames: Array<any>) {
    if (typeof arguments[0] === "function") {
      // inject 的第一个参数是一个函数
        let grabStoresFn = arguments[0]
        return (componentClass: React.ComponentClass<any, any>) =>
            createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true)
    } else {
      // inject 的参数是 store 
        return (componentClass: React.ComponentClass<any, any>) =>
            createStoreInjector(
                grabStoresByName(storeNames),
                componentClass,
                storeNames.join("-"),
                false
            )
    }
}

首先来看一下 inject 源码的主体函数:

/**
* 为 inject 函数提供多个函数类型定义实现函数重载
*/

// inject 的参数为 store 时的函数类型定义
export function inject(
    ...stores: Array<string>
): <T extends IReactComponent<any>>(
    target: T
) => T & (T extends IReactComponent<infer P> ? IWrappedComponent<P> : never)

// inject 的参数为function 时的函数类型定义
export function inject<S, P, I, C>(
    fn: IStoresToProps<S, P, I, C>
): <T extends IReactComponent>(target: T) => T & IWrappedComponent<P>

  
export function inject(/* fn(stores, nextProps) or ...storeNames */ ...storeNames: Array<any>) {
    if (typeof arguments[0] === "function") {
      // inject 的第一个参数是一个函数
        let grabStoresFn = arguments[0]
        return (componentClass: React.ComponentClass<any, any>) =>
            createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true)
    } else {
      // inject 的参数是 store 
        return (componentClass: React.ComponentClass<any, any>) =>
            createStoreInjector(
                grabStoresByName(storeNames),
                componentClass,
                storeNames.join("-"),
                false
            )
    }
}

源码中:

  1. 在实现 inject 之前,首先提供两个函数类型定义对inject函数进行函数重载。第一个定义是inject的参数为 store 时的函数类型定义,第二个定义是inject的参数为 store name 时的函数类型定义。因此,inject 的传参支持function和store name。

  2. 当 inject 的参数是function时,调用 createStoreInjector 方法创建一个响应式的新组件,createStoreInjector方法的参数分别是用户传递进来的function 和 component。

  3. 当 inject 的参数是 store name 时,同样是调用 createStoreInjector 方法创建一个新组件,但是createStoreInjector 参数会不一样,其中第一个参数是调用 grabStoresByName 函数,返回合并 store name 后的新 props。createStoreInjector 的最后一个参数传递 false,表明当前创建的新组件不是响应式的。

接下来我们分析一下用户创建新组件的 createStoreInject 方法:

function createStoreInjector(
    grabStoresFn: IStoresToProps,
    component: IReactComponent<any>,
    injectNames: string,
    makeReactive: boolean
): IReactComponent<any> {
    // Support forward refs
    let Injector: IReactComponent<any> = React.forwardRef((props, ref) => {
        const newProps = { ...props }
        const context = React.useContext(MobXProviderContext)
        // 合并 props
        // grabStoresFn: 执行用户传递进来的返回新props的函数
        Object.assign(newProps, grabStoresFn(context || {}, newProps) || {})

      // 将 ref 对象添加到新组件的 props 上
        if (ref) {
            newProps.ref = ref
        }
                
        // 根据传进来的组件和 props 创建一个新的组件
        return React.createElement(component, newProps)
    })

    // inject 的第一个参数是函数时,将 Injector 变成响应式组件
    if (makeReactive) Injector = observer(Injector)
    Injector["isMobxInjector"] = true // assigned late to suppress observer warning

    // Static fields from component should be visible on the generated Injector
    copyStaticProperties(component, Injector)
    Injector["wrappedComponent"] = component
        // 给 Injector 组件添加 组件的显示名称
    Injector.displayName = getInjectName(component, injectNames)
    return Injector
}

在 createStoreInjector 中,使用 React 的底层API createElement() 创建一个新的 React Component,然后再使用 React.forwardRef 将 createElement 创建的组件转换成支持 refs 转发的新组件。然后根据 makeReactive 参数来将新组件转换成响应式组件。当 inject 的参数是一个function 时,即 makeReactive 会设置为true,会使用 mobx-react 的observer 将新组件转换成响应式组件,当 inject 的参数是一个function 时,即 makeReactive 为false,新组件不是一个响应式组件。

Provider 与 inject 实现

上面我们分别分析了 Provider 和 inject 的原理,下面,我们就根据它们的原理,分别实现一个简单版的 Provider 和 inject。

实现 Provider

import React, {useRef, useContext} from "react";
// 导入 Context 上下文对象
import {MobXProviderContext} from "./MobXProviderContext";

export function Provider({children, ...stores}) {
  // 使用 useContext hook 获取当前 Context 上下文对象的当前值
  const parentValue = useContext(MobXProviderContext);
  // 使用 useRef hook 创建一个可变的 ref 对象,它的 .current 属性的初始值为当前的 Context 上下文对象的当前值和 stores
  const mutableProvdierRef = useRef({...parentValue, ...stores});
  // 通过 ref 对象的 current 属性获取保存在 ref 对象中的 Context 上下文对象当前值和 stores
  const value = mutableProvdierRef.current;
  
  // 每个 Context 对象都会返回一个 Provider React 组件
    // 将存储在可变对象ref中的  Context 上下文对象的当前值和 stores 传递给 Context 上下文对象的 value属性
    // 当 value 值发生改变时,Provider 内部的子组件都会重新渲染
  return (
    <MobXProviderContext.Provider value={value}>
      {children}
    </MobXProviderContext.Provider>
  );
}

实现 inject

import React, {useContext} from "react";
import {MobXProviderContext} from "./MobXProviderContext";

export const inject = (...storeNames) => component => {
  // 使用 forwardRef 创建一个支持 refs 转发的组件
  const Injector = React.forwardRef((props, ref) => {
    // 使用 useContext hook 获取当前 Context 上下文对象的当前值
    const context = useContext(MobXProviderContext);
    const newProps = {
      ...props,
      ...context
    };
    if (ref) {
      newProps.ref = ref;
    }
    // 根据传进来的组件和 props 创建一个新的组件
    return React.createElement(component, newProps);
  });

  return Injector;
};

Provider 和 inject 实现代码详细参考:github.com/moozisheng/…