Mobx 源码解析(一):从 makeAutoObservable 出发看响应式原理

387 阅读5分钟

Hi!我是 Jet ,我准备在最近更新系列关于状态管理的源码解读(包含 redux、mobx、jotai),从平时开发中常用的 api 延展,欢迎关注

MobX 的主要思想是用「函数响应式编程」和「可变状态模型」使得状态管理变得简单和可扩展。

实现上是 mutable 的思想 + proxy(为了兼容性,proxy 实际上使用 Object.defineProperty 实现)。

本系列解析所使用的 mobx 版本是 6.12 , mobx-react-lite 版本是 3.1

首先是一个 todolist 的代码案例

import React from "react";
import ReactDOM from "react-dom";
import { makeAutoObservable, toJS } from "mobx";
import { observer } from "mobx-react-lite";
import "./App.css";

// 创建store
class TodoStore {
  todos: Todo[] = [];
  listLength: number = 0;

  constructor() {
    makeAutoObservable(this);
  }

  addTodo = (text: string) => {
    this.todos.push({
      id: Date.now(),
      text,
      completed: false,
    });
    this.listLength = this.todos.length;
  };

  toggleTodo = (id: number) => {
    this.todos = this.todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    this.listLength = this.todos.length;
  };
}

const todoStore = new TodoStore();

console.log(toJS(todoStore));

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoProps {
  todo: Todo;
  toggleTodo: () => void;
}

const Todo: React.FC<TodoProps> = ({ todo, toggleTodo }) => {
  return (
    <li className={todo.completed ? "completed" : ""} onClick={toggleTodo}>
      {todo.text}
    </li>
  );
};

const TodoList = observer(() => {
  const [text, setText] = React.useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim() !== "") {
      todoStore.addTodo(text);
      setText("");
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button type="submit">Add Todo</button>
      </form>
      <p>List Length: {todoStore.listLength}</p>
      <ul>
        {todoStore.todos.map((todo) => (
          <Todo
            key={todo.id}
            todo={todo}
            toggleTodo={() => todoStore.toggleTodo(todo.id)}
          />
        ))}
      </ul>
    </div>
  );
});

ReactDOM.render(<TodoList />, document.getElementById("root"));

实现效果图:

image.png

案例拥有增加和修改功能,可以看到,我们只使用了两个 api 就完成了这个案例,非常简便,一个是 mobxmakeAutoObservable,另一个是 mobx-react-liteobserver,那么我们先从 makeAutoObservable 开始讲起

makeAutoObservable

makeAutoObservable 是 MobX 6 版本中新引入的 API,将普通 JavaScript 对象或类转换为可观察对象的函数。它们可以自动地将类的属性声明为可观察状态,并根据需要生成相应的 getter 和 setter 方法。我们直接看 makeAutoObservable 的源码实现

export function makeAutoObservable(target, overrides, options){
    // 判断是否是普通对象(原型为 Object)
    // 如果是,调用 extendObservable
    // 在实例和日常开发中,一般在 class 的 constructor 使用 makeAutoObservable
    // 并非直接继承于 Object,不会走这个 if
    if (isPlainObject(target)) {
        return extendObservable(target, target, overrides, options)
    }

    // initObservable 作用是开启批处理,并允许 mobx 的 global state 更改,并执行传入的函数
    initObservable(() => {
        // const $mobx = Symbol("mobx administration") $mobx 是一个特殊的 symbol key
        // 创建了一个 ObservableObjectAdministration 对象,添加到原有的对象上,key 为上面的 $mobx
        const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]

        // const keysSymbol = Symbol("mobx-keys") keysSymbol 也是一个特殊的 symbol key
        // 获取对象的原型,本身和原型上的 key ,删除 constructor 和 $mobx 后,构建 set,存储这些 key 放到原型的 keysSymbol 上
        if (!target[keysSymbol]) {
            const proto = Object.getPrototypeOf(target)
            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
}

打印 store 的实例可以看到,属性上有一个 mobx administrationObservableObjectAdministration 对象,而原型上有一个 mobx-keys 的 set,保存了所有的 key

image.png

从名字就可以看出来 ObservableObjectAdministration 是作为管理 target 对象的 “管理员”,我们看一下 adm.make_ 也就是 ObservableObjectAdministration.make_ 是如何做响应式处理的

ObservableObjectAdministration.make_

make_(key: PropertyKey, annotation: Annotation | boolean): void {
    if (annotation === true) {
        annotation = this.defaultAnnotation_
    }
    if (annotation === false) {
        return
    }
    // 校验参数,确保存在
    if (!(key in this.target_)) {
        if (this.target_[storedAnnotationsSymbol]?.[key]) {
            return
        } else {
            die(1, annotation.annotationType_, `${this.name_}.${key.toString()}`)
        }
    }
    let source = this.target_
    // 在原型链上查找属性
    while (source && source !== objectPrototype) {
        // 就是 Object.getOwnPropertyDescriptor
        const descriptor = getDescriptor(source, key)
        if (descriptor) {
            // 使用 annotation 注解对象处理这个 key 对应的值,将其做响应式处理
            const outcome = annotation.make_(this, key, descriptor, source)
            if (outcome === MakeResult.Cancel) {
                return
            }
            if (outcome === MakeResult.Break) {
                break
            }
        }
        source = Object.getPrototypeOf(source)
    }
    // 记录这个值已经做了响应式处理,其实这个函数做的事情是删除原有的属性,让其 get 走响应后的值
    recordAnnotationApplied(this, annotation, key)
}

可以看到先是做了一些校验和查找,然后调用了 annotation.make_(this, key, descriptor, source) 中,继续看这个函数做了什么

annotation.make_

// 1. 如果有 get 处理为 computed value
// 2. 单独绑定 value 的 set 处理为 action
// 3. 原型上的方法,生成器函数处理为 flow,其余的处理为 autoAction
// 4. 其余的变为 observable (普通)
function make_(
    adm: ObservableObjectAdministration,
    key: PropertyKey,
    descriptor: PropertyDescriptor,
    source: object
): MakeResult {
    // 1. get 处理为 computed value
    if (descriptor.get) {
        return computed.make_(adm, key, descriptor, source)
    }
    // 2. 单独绑定 value 的 set 处理为 action
    if (descriptor.set) {
        const set = createAction(key.toString(), descriptor.set) as (v: any) => void
        if (source === adm.target_) {
            return adm.defineProperty_(key, {
                configurable: globalState.safeDescriptors ? adm.isPlainObject_ : true,
                set
            }) === null
                ? MakeResult.Cancel
                : MakeResult.Continue
        }
        defineProperty(source, key, {
            configurable: true,
            set
        })
        return MakeResult.Continue
    }
    // 原型上的方法,生成器函数处理为 flow,其余的处理为 autoAction
    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)
    }
    let observableAnnotation = this.options_?.deep === false ? observable.ref : observable
    if (typeof descriptor.value === "function" && this.options_?.autoBind) {
        descriptor.value = descriptor.value.bind(adm.proxy_ ?? adm.target_)
    }
    // 4. 其余的变为 observable
    return observableAnnotation.make_(adm, key, descriptor, source)
}

可以看到 make_ 对于属性的配置做了分类,从而对不同类型的值做了不同的处理,我们定义的普通的值会被进行 observableAnnotation.make_ 处理,我们追踪一下这个方法的继续调用

observableAnnotation.make_(adm, key, descriptor, source) ->

observableAnnotation.extend_(adm, key, descriptor, false) ->

ObservableObjectAdministration.defineObservableProperty_(key, descriptor.value, this.options_?.enhancer ?? deepEnhancer, false)

最终回到了 ObservableObjectAdministration 上,那我们来看一下 defineObservableProperty_

ObservableObjectAdministration.defineObservableProperty_

defineObservableProperty_(
  key: PropertyKey,
  value: any,
  enhancer: IEnhancer<any>,
  proxyTrap: boolean = false
): boolean | null {
  checkIfStateModificationsAreAllowed(this.keysAtom_)
  try {
      startBatch()
      const deleteOutcome = this.delete_(key)
      if (!deleteOutcome) {
          return deleteOutcome
      }
      if (hasInterceptors(this)) {
          const change = interceptChange<IObjectWillChange>(this, {
              object: this.proxy_ || this.target_,
              name: key,
              type: ADD,
              newValue: value
          })
          if (!change) {
              return null
          }
          value = (change as any).newValue
      }
      // get 返回为 getObservablePropValue_ ,set 返回为 setObservablePropValue_ 
      const cachedDescriptor = getCachedObservablePropDescriptor(key)
      const descriptor = {
          configurable: globalState.safeDescriptors ? this.isPlainObject_ : true,
          enumerable: true,
          get: cachedDescriptor.get,
          set: cachedDescriptor.set
      }
      // 这里会走 else 分支
      if (proxyTrap) {
          if (!Reflect.defineProperty(this.target_, key, descriptor)) {
              return false
          }
      } else {
          // 重点,将该 key 的 配置(get,set 等)用 defineProperty
          // (即 Object.defineProperty)添加到对象上
          defineProperty(this.target_, key, descriptor)
      }
      // 将该值处理为 ObservableValue
      const observable = new ObservableValue(
          value,
          enhancer,
          __DEV__ ? `${this.name_}.${key.toString()}` : "ObservableObject.key",
          false
      )
      // this.values_ 是一个 map,保存该 key 的 值
      this.values_.set(key, observable)
      // 通知该 key 的 Add 事件
      this.notifyPropertyAddition_(key, observable.value_)
  } finally {
      endBatch()
  }
  return true
}

看完这个函数,我们可以发现值的 get 被设置为了 ObservableObjectAdministration.getObservablePropValue_ ,我们看一下这个函数

getObservablePropValue_(key: PropertyKey): any {
    return this.values_.get(key)!.get()
}

可以看到我们取值时,其实是从 ObservableValue 的实例中的 values_ 中取值

defineObservableProperty_ 中,我们刚刚看到 values_ 中保存的值都是 ObservableValue 的实例,也就是说,我们是从 ObservableValue.get 中取的值。

至此,我们看到了响应式的一个核心类:ObservableValue ,它表示可观察的单一值。在 MobX 中,被观察的值可以是任何类型的 JavaScript 值,比如数字、字符串、对象等等。文档对他的介绍是:

image.png

关于 ObservableValue 的源码实现,我们下一个文章再更,而且我们还剩下 mobx 对 set 方法做了哪些处理还没有说,这个留在后面再具体说,这期先写到这