MrErHu/blog

MobX enforceActions使用总结

MrErHu opened this issue · 0 comments

enforceActions作用

The goal of enforceActions is that you don't forget to wrap event handlers in action.

Possible options:

  • "observed" (default): All state that is observed somewhere needs to be changed through actions. This is the default, and the recommended strictness mode in non-trivial applications.
  • "never": State can be changed from anywhere.
  • "always": State always needs to be changed through actions, which in practice also includes creation.

The benefit of "observed" is that it allows you to create observables outside of actions and modify them freely, as long as they aren't used anywhere yet.

Since state should in principle always be created from some event handlers, and event handlers should be wrapped, "always" captures this the best. But you probably don't want to use this mode in unit tests.

In the rare case where you create observables lazily, for example in a computed property, you can wrap the creation ad-hoc in an action using runInAction.

按照MobX的文档描述,enforceActions的目的是不要忘记给事件处理函数包裹actions。其取值分别为observed、never、always。

  • observed 所有被observed的变量改变必须使用actions包裹。是正式项目的推荐严格模式。
  • never: 任何变量都是随处被更改
  • always: 任何变量的更改都需要包裹actions,包括变量在创建时。
    之所以推荐使用observed是因为在创建变量或者变量在使用是,允许自由更改。

主要注意的是,MobX4/5中默认的enforceActions是never,而MobX默认的enforceActions是observed

MobX内部实现

以JDY项目中使用的MobX4为例。在Object.defineProperty中的set触发时,会触发到:

export function checkIfStateModificationsAreAllowed(atom: IAtom) {
    const hasObservers = atom.observers.length > 0
    // Should never be possible to change an observed observable from inside computed, see #798
    if (globalState.computationDepth > 0 && hasObservers)
        fail(
            process.env.NODE_ENV !== "production" &&
                `Computed values are not allowed to cause side effects by changing observables that are already being observed. Tried to modify: ${
                    atom.name
                }`
        )
    // Should not be possible to change observed state outside strict mode, except during initialization, see #563
    if (!globalState.allowStateChanges && (hasObservers || globalState.enforceActions === "strict"))
        fail(
            process.env.NODE_ENV !== "production" &&
                (globalState.enforceActions
                    ? "Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an `action` if this change is intended. Tried to modify: "
                    : "Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, the render function of a React component? Tried to modify: ") +
                    atom.name
        )
}

从上面的代码中可以得出:

如果globalState.allowStateChanges === false 表示当前不允许修改state,如果enforce是严格模式('always')或者state已经存在监听(atom.observers.length > 0)则会理解报错。

我们可以同归下面的configure代码逻辑中佐证我们的想法。

// 代码做了精简
export function configure(options: {
    enforceActions?: boolean | "strict" | "never" | "always" | "observed"
}): void {
    const { enforceActions } = options
    if (enforceActions !== undefined) {
        if (typeof enforceActions === "boolean" || enforceActions === "strict")
            deprecated(
                `Deprecated value for 'enforceActions', use 'false' => '"never"', 'true' => '"observed"', '"strict"' => "'always'" instead`
            )
        let ea
        switch (enforceActions) {
            case true:
            case "observed":
                ea = true
                break
            case false:
            case "never":
                ea = false
                break
            case "strict":
            case "always":
                ea = "strict"
                break
            default:
                fail(
                    `Invalid value for 'enforceActions': '${enforceActions}', expected 'never', 'always' or 'observed'`
                )
        }
        globalState.enforceActions = ea
        globalState.allowStateChanges = ea === true || ea === "strict" ? false : true
    }
}

我们以内部的runInAction为例,runInAction内部最终实现的的逻辑在:executeAction,而executeAction基本逻辑是:

export function executeAction(actionName: string, fn: Function, scope?: any, args?: IArguments) {
    const runInfo = startAction(actionName, fn, scope, args)
    let shouldSupressReactionError = true
    try {
        const res = fn.apply(scope, args)
        shouldSupressReactionError = false
        return res
    } finally {
        if (shouldSupressReactionError) {
            globalState.suppressReactionErrors = shouldSupressReactionError
            endAction(runInfo)
            globalState.suppressReactionErrors = false
        } else {
            endAction(runInfo)
        }
    }
}

其中startAction函数中通过调用allowStateChangesStart将globalState.allowStateChanges设置为true,而endAction中又将globalState.allowStateChanges恢复成原来的值。

实际工作中存在的问题

为什么下面代码会报错?

@action
getField() {
    return post(api).then(({ data }) => this.fields = get(data, 'systemFields'));
}

首先@action做了什么?action是一个decorator,其内部最终调用函数:createAction → executeAction

export function createAction(actionName: string, fn: Function): Function & IAction {
    if (process.env.NODE_ENV !== "production") {
        invariant(typeof fn === "function", "`action` can only be invoked on functions")
        if (typeof actionName !== "string" || !actionName)
            fail(`actions should have valid names, got: '${actionName}'`)
    }
    const res = function() {
        return executeAction(actionName, fn, this, arguments)
    }
    ;(res as any).isMobxAction = true
    return res as any
}

因为post中的处理函数是异步的,因此在执行时executeAction早已经执行了endAction,因此此时globalState.enforceActions是false。而@action的实现逻辑上

action.bound = boundActionDecorator as any;
 
export function defineBoundAction(target: any, propertyName: string, fn: Function) {
    addHiddenProp(target, propertyName, createAction(propertyName, fn.bind(target)))
}

action.bound相比于action只是在createAction中将需要装饰的函数强制绑定了this。并没有其他的区别。