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 源码中:
-
首先使用 useContext hook 创建一个 React Context上下文对象,以便于在 Provider 组件获取 Context 上下文对象的当前值 和 React Context 对象的 Provider React 组件;
-
然后创建一个名为 Provider的函数组件,在函数组件中获取Context 上下文对象的当前值,将当前值和 stores 保存在可变的 ref 对象中
-
最后将存储在可变对象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
)
}
}
源码中:
-
在实现 inject 之前,首先提供两个函数类型定义对inject函数进行函数重载。第一个定义是inject的参数为 store 时的函数类型定义,第二个定义是inject的参数为 store name 时的函数类型定义。因此,inject 的传参支持function和store name。
-
当 inject 的参数是function时,调用 createStoreInjector 方法创建一个响应式的新组件,createStoreInjector方法的参数分别是用户传递进来的function 和 component。
-
当 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/…