一个栗子
ts
class Todo {
private title = ''
constructor() {
makeAutoObservable(this)
}
aaa() {
console.log('aaa');
}
bbb = () => {
console.log('bbb');
}
ccc = function() {
console.log('ccc');
}
}
例子中,title 是一个属性,aaa是原型上的方法,bbb和ccc都是实例方法。
向gpt4进行如下提问
根据官方文档的分析
如果你阅读过mobx的官方文档,你一定会知道makeAutoObservable
做了什么。文档所说如下:
makeAutoObservable
就像是加强版的 makeObservable
,在默认情况下它将推断所有的属性。
推断规则:
- 所有 自有 属性都成为
observable
。 - 所有
get
ters 都成为computed
。 - 所有
set
ters 都成为action
。 - 所有 prototype 中的 functions 都成为
autoAction
。 - 所有 prototype 中的 generator functions 都成为
flow
。(需要注意,generators 函数在某些编译器配置中无法被检测到,如果 flow 没有正常运行,请务必明确地指定flow
注解。) - 在
overrides
参数中标记为false
的成员将不会被添加注解。例如,将其用于像标识符这样的只读字段。
根据推断规则 ,title 标记为observable
,aaa()标记为autoAction
,bbb() 和ccc()被标记为observable
。
此时的你是否会有疑惑:
- 一向以计算机技术专家著称的gpt4,为什么它的回答与你所看的文档有如此大的出入?
- bbb()和ccc()成为
observable
好象也没有太大的意义,难道是文档是旧版没有更新吗? - 究竟谁是对的?
尽信书不如无书,为了确认makeAutoObservable
做了什么,我们就自己去看看。
真相探究
从源码的角度
下面所示源码均已简化逻辑:
ts
// makeAutoObservable 方法的实现
export function makeAutoObservable<T extends object, AdditionalKeys extends PropertyKey = never>(
target: T,
overrides?: AnnotationsMap<T, NoInfer<AdditionalKeys>>,
options?: MakeObservableOptions
): T {
// 如果target是纯Object(构造方法为Object的对象)则执行该逻辑;
// 但我们在类的构造函数中执行该方法时target是this,this是类的实例,
// 它的构造方法指向该类的定义,所以不会执行下面的逻辑
if (isPlainObject(target)) {
return extendObservable(target, target, overrides, options)
}
// 执行该方法
initObservable(() => {
const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
if (!target[keysSymbol]) {
const proto = Object.getPrototypeOf(target)
// 收集类中属性和原型上的所有key值
const keys = new Set([...ownKeys(target), ...ownKeys(proto)])
keys.delete("constructor")
keys.delete($mobx)
addHiddenProp(proto, keysSymbol, keys)
}
// 遍历所有key值,并赋予标识
target[keysSymbol].forEach(key =>
adm.make_(
key,
// must pass "undefined" for { key: undefined }
!overrides ? true : key in overrides ? overrides[key] : true
)
)
})
return target
}
ts
export const autoAnnotation: Annotation = createAutoAnnotation()
export function createAutoAnnotation(options?: object): Annotation {
return {
annotationType_: AUTO,
options_: options,
make_,
extend_,
decorate_20223_
}
}
// adm的make_方法
// key值为定义上类上的每一个属性和方法,annotation值为makeAutoObservable的overrides参数解析
// 由于overrides没有传所有这里的值是true
make_(key: PropertyKey, annotation: Annotation | boolean): void {
if (annotation === true) {
// 该默认annotation是autoAnnotation,如上所示
annotation = this.defaultAnnotation_
}
if (annotation === false) {
return
}
assertAnnotable(this, annotation, key)
if (!(key in this.target_)) {
...略
}
let source = this.target_
// 遍历属性和原型上的属性,并赋予标识
while (source && source !== objectPrototype) {
const descriptor = getDescriptor(source, key)
if (descriptor) {
// 执行autoAnnotation上的make_方法对每一个定义在类上的属性和方法赋予标识
const outcome = annotation.make_(this, key, descriptor, source)
if (outcome === MakeResult.Cancel) {
return
}
if (outcome === MakeResult.Break) {
break
}
}
source = Object.getPrototypeOf(source)
}
recordAnnotationApplied(this, annotation, key)
}
ts
// autoAnnotation上的make_方法
function make_(
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
source: object
): MakeResult {
// getter 赋予 computed
if (descriptor.get) {
return computed.make_(adm, key, descriptor, source)
}
// lone setter -> action setter
if (descriptor.set) {
...略
}
// function on proto 赋予 autoAction/flow
if (source !== adm.target_ && typeof descriptor.value === "function") {
if (isGenerator(descriptor.value)) {
const flowAnnotation = this.options_?.autoBind ? flow.bound : flow
return flowAnnotation.make_(adm, key, descriptor, source)
}
const actionAnnotation = this.options_?.autoBind ? autoAction.bound : autoAction
return actionAnnotation.make_(adm, key, descriptor, source)
}
// other 赋予 observable
// Copy props from proto as well, see test:
// "decorate should work with Object.create"
let observableAnnotation = this.options_?.deep === false ? observable.ref : observable
// if function respect autoBind option
if (typeof descriptor.value === "function" && this.options_?.autoBind) {
descriptor.value = descriptor.value.bind(adm.proxy_ ?? adm.target_)
}
return observableAnnotation.make_(adm, key, descriptor, source)
}
由源码可知道,title、 bbb() 和ccc()标记为observable
,aaa()标记为autoAction
。我们再从实验的角度来验证一下。
实验的角度
ts
import {makeAutoObservable, runInAction, autorun} from 'mobx'
class Todo {
private title = ''
constructor() {
makeAutoObservable(this)
}
aaa() {
console.log('aaa');
}
bbb = () => {
console.log('bbb');
}
ccc = function() {
console.log('ccc');
}
}
test1:
ts
const todo = new Todo;
autorun(() => {
console.log(todo.aaa)
})
runInAction(() => {
todo.aaa = 1
})
// 输出:
// [Function: aaa] { isMobxAction: true, toString: [Function (anonymous)] }
test2:
ts
const todo = new Todo;
autorun(() => {
console.log(todo.bbb)
})
runInAction(() => {
todo.bbb = 1
})
// 输出:
// [Function: Todo@1.bbb] { isMobxAction: true, toString: [Function (anonymous)] }
// 1
test3:
ts
const todo = new Todo;
autorun(() => {
console.log(todo.ccc)
})
runInAction(() => {
todo.ccc = 1
})
// 输出:
// [Function: Todo@1.ccc] { isMobxAction: true, toString: [Function (anonymous)] }
// 1
由测试结果所示,bbb()和ccc() 确定被标记为了observable
。
结论及推论
根据我们的源码探究和实验验证,我们得出了如下的结论及推论:
- 官网的说明是正确的
- 属性方法被自动标记为
observable
,在该方法中执行多个修改值的操作不会被视为同一个事务,而且该属性的可观测性只体现在重新赋值该属性。 - 正确定义
autoAction
需要写成aaa
方法的模式,不可写成bbb
和ccc
方法的模式,bbb
和ccc
方法虽然也可以触发action
的行为,但没有action
基于事务的批处理优化 - 对于
aaa
方法的this指向问题,我们可以在定义makeAutoObservable
方法的时候写成makeAutoObservable(this, {}, { autoBind: true })
,这样就解决了aaa()方法内原型指向的问题。 autoAction
是action
标识的子集,相比action
简化了异步流程,只要是被标记为autoAction
的方法,在其中使用异步逻辑即可不再手动将赋值逻辑包裹在runInAction
方法中。makeAutoObservable
的第三参options
的默认值{autoBind: false, deep: true}