Mobx 源码解读(六) Action

Mobx 提供 action API 供用户声明修改应用状态的函数。Mobx 进行了一层简单的包装,提供事务功能,可以在 action 内进行多次 Observable 的修改,而不用担心 Reaction 的多次重新执行;同时支持 spy,可以在开发工具中观察到 action 的执行。

action API 的入口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var action: IActionFactory = function action(arg1, arg2?, arg3?, arg4?): any {
// action(fn)
if (arguments.length === 1 && typeof arg1 === "function")
return createAction(arg1.name || "<unnamed action>", arg1)
// action(name, fn)
if (arguments.length === 2 && typeof arg2 === "function") return createAction(arg1, arg2)

// @action(name)
if (arguments.length === 1 && typeof arg1 === "string") return namedActionDecorator(arg1)

// @action
// arg2 就是属性名,作为默认 name
return namedActionDecorator(arg2).apply(null, arguments)
} as any

Action 原理

先来看 createAction 函数的实现:

1
2
3
4
5
6
7
8
9
10
function createAction(actionName: string, fn: Function): Function & IAction {
const res = function() {
return executeAction(actionName, fn, this, arguments)
}
// 原函数作为属性添加到包装后的函数上
;(res as any).originalFn = fn
// 标志位
;(res as any).isMobxAction = true
return res as any
}

可见,只是返回了一个经过 executeAction 包装的函数。executeAction 在原函数的执行前后,分别调用了 startAction 和 endAction:

1
2
3
4
5
6
7
8
function executeAction(actionName: string, fn: Function, scope?: any, args?: IArguments) {
const runInfo = startAction(actionName, fn, scope, args)
try {
return fn.apply(scope, args)
} finally {
endAction(runInfo)
}
}

startAction 会先调用 untrackedStart 函数,将当前(可能有)正在跟踪的 Derivation 置为 null,避免依赖收集。然后调用 startBatch 开始一个新事务,确保 action 结束后才开始 Derivation 的重新计算。最后,将全局的标志位 allowStateChanges 置为 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function startAction(
actionName: string,
fn: Function,
scope: any,
args?: IArguments
): IActionRunInfo {
// 可能有 Derivation 正在重新计算,先暂停
const prevDerivation = untrackedStart()
startBatch()
// 全局标志 allowStateChange 置为 true
const prevAllowStateChanges = allowStateChangesStart(true)
return {
prevDerivation,
prevAllowStateChanges,
}
}

// src/core/derivation.ts
function untrackedStart(): IDerivation | null {
const prev = globalState.trackingDerivation
globalState.trackingDerivation = null
return prev
}

action 执行完后,调用 endAction 还原两个全局变量,结束事务。

1
2
3
4
5
function endAction(runInfo: IActionRunInfo) {
allowStateChangesEnd(runInfo.prevAllowStateChanges)
endBatch()
untrackedEnd(runInfo.prevDerivation)
}

作为装饰器使用

在 action 入口函数中会进入第三、四个分支,调用 namedActionDecorator 生成一个装饰器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function namedActionDecorator(name: string) {
// 这个函数只是做了一些边界条件的检查
return function(target, prop, descriptor) {
if (descriptor && typeof descriptor.value === "function") {
descriptor.value = createAction(name, descriptor.value)
descriptor.enumerable = false
descriptor.configurable = true
return descriptor
}
if (descriptor !== undefined && descriptor.get !== undefined) {
throw new Error("[mobx] action is not expected to be used with getters");
}
// 实际上装饰器函数还是通过 createClassPropertyDecorator 创建的
return actionFieldDecorator(name).apply(this, arguments)
}
}

同样是使用 createClassPropertyDecorator 生成装饰器函数(参看第三篇):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const actionFieldDecorator = createClassPropertyDecorator(
function(target, key, value, args, originalDescriptor) {
const actionName =
args && args.length === 1 ? args[0] : value.name || key || "<unnamed action>"
const wrappedAction = action(actionName, value)
// 将包装后的 action 作为隐藏属性添加到类实例上
addHiddenProp(target, key, wrappedAction)
},
function(key) {
return this[key]
},
function() {
// 不允许赋值
invariant(false, getMessage("m001"))
},
false,
true
)

这样在类实例化时,包装好的 action 就作为隐藏属性添加到了类实例上。

action.bound 绑定上下文

在 creatAction 函数中可以看到,action 使用默认的上下文:

1
2
3
const res = function() {
return executeAction(actionName, fn, this, arguments)
}

使用 action.bound 可以将 this 绑定到对象或类实例上:

1
2
3
4
5
6
7
8
function defineBoundAction(target: any, propertyName: string, fn: Function) {
const res = function() {
// 上下文对象为 target
return executeAction(propertyName, fn, target, arguments)
}
;(res as any).isMobxAction = true
addHiddenProp(target, propertyName, res)
}

runInAction

action(fn)() 的语法糖,直接调用了 executeAction:

1
2
3
4
5
6
7
function runInAction<T>(arg1, arg2?, arg3?) {
const actionName = typeof arg1 === "string" ? arg1 : arg1.name || "<unnamed action>"
const fn = typeof arg1 === "function" ? arg1 : arg2
const scope = typeof arg1 === "function" ? arg2 : arg3

return executeAction(actionName, fn, scope, undefined)
}