阅读 706

Vue 3.0 RFC API 的实现

Vue3.0 的 RFC 已经发布了几个月了,Vue 底层几乎没有变动,还是沿用原来响应式的。所以一直在思考能不能使用现在的版本,实现 RFC 中的 API,直到看到了 Vue Function API 这个库,这个库让开发者提前尝鲜到了RFC 中的 API,当然作为 RFC,所以最终 3.0 的 API 还是未知的,以及底层的实现也还未知。

Dreamacro大佬说 类型才是 functional 的最大意义。我觉得非常有道理,在这个 ts 盛行的年代。

一个思考

想复用逻辑和状态,关键在于如何创建一个可以被 Vue 观察的对象(响应式对象)。当响应式的对象发生了变化时,Vue 会开始它的更新逻辑,至于它是怎么更新了,这里不作讨论。其次就是,怎么将这个状态绑定到 vm 上,除了使用 computed 来手动绑定之外,还可以用什么方法。

Ovservable

在 Vue 2.6 之前,想创建一个响应式对象需要实例化一个 Vue,但在 Vue 2.6 之后,可以通过Vue.observable 来创建一个响应式的对象。

Vue 2.6 以前

const create = (obj: object) => {
  const vm = new Vue({ data: () => obj })
  return vm.$data
}
复制代码

Vue 2.6 之后

const create = (obj: object) => Vue.observable(obj)
复制代码

这里有一个 DEMO,可以看出,普通的对象更新是不会触发 Vue 的更新逻辑的。响应式的对象,即使不在该 Vue 实例中去更改值,也会触发 Vue 的更新,可以在 DEMO 的控制台中尝试一下输入 x.value++

简单的DEMO

DEMO 中,不处理上下文,不管任何生命周期,只想表达 setup 以及 value 是如何工作的。

首先考虑一下这个 value,当这个 value 的类型为 number 或者 string 等非对象的类型时,为了创建一个响应式的对象,所以需要一层 wrapper ,这样 Vue 才能创建一个响应式对象。这就是为什么 RFC 中使用 valuevariable.value 的形式了。

但是如果 value 本身是一个对象的话,可以不需要这层 wrapper 。可能为了统一,所以都加上了这层。

其次 setup 作为一个 Vue的配置,需要在 Vue 实例化的时候执行的,选择 Vue 的第一个生命周期 beforeCreate 钩子中执行这个函数,等于用 setup 函数来替代 beforeCreate,这样可以在 setup 函数中使用其他生命周期的钩子。

最后是这个 setup 的返回值,如何 unwrapped 并将值挂到 this 上提供给 template 使用。

这里提供一个最简单的 DEMO 可以看 vfp.js 的实现,仅仅在 beforeCreate 执行了一下 setup并将返回值做一层 unwrapped 并挂载到 vm 上 提供给 template 使用。

Vue-Function-API 源码解析

注: vm => Vue实例

value和state

先来看几个关于 wrapper 的类。

AbstractWrapper.ts

这个抽象类主要实现了一个 setVmProperty 的方法,主要用来将 value 这个挂载到 vm上。

ValueWrapper.ts

这个类就是 value 函数实际使用的类,继承 AbstractWrapper,主要实现了 value getvalue set并且约定使用 ?state 作为响应式对象中的 key

ComputedWrapper.ts

这个类主要用于对 computed 做一层 wrapper,继承 AbstractWrapper

state 函数主要就是将对象转换成响应式的对象,所以方法也及其简单。

export function state<T>(value: T): T {
  return observable(value);
}
复制代码

value 函数需要将响应式对象通过 类ValueWrapper 包装一层

export function value<T>(value: T): Wrapper<T> {
  return new ValueWrapper(state({ ?state: value }));
}
复制代码

这里的 key: ?state 也可以使用其他的。

valuestate 函数都比较简单,目的就是创建响应式对象。

valuestate 的区别在于,value 方法可以将一个非对象的类型(number 、 string 、 boolean),包装成一个响应式对象。而 state 可以直接将一个非响应式对象包装成一个响应式对象。

const A = state({ value: 0 })
const B = value(0)

// 这两者在某种意义上是等价的。
复制代码

setup

setup.ts

主要有3个方法 functionApiInit initSetup createSetupContext

初始化

在 Vue 生命周期中的 beforeCreate 执行 functionApiInit。主要用于判断是否在 Vue 的配置中存不存在 setup 方法,如果存在,就进行 initSetup

执行

initSetup方法中,先通过 createSetupContext 方法创建一个自己的上下文。然后通过一个全局变量,保存上一次的 vm,再设置当前的 vm,接下去将之前创建的 上下文以及props 作为参数执行 setup 函数。执行结束后,将当前的 vm 还原为上一次的 vm。最后将 setup 的返回值,绑定到 vm 中。

这里为什么需要保存之前所执行的上下文?issue

setup 是可以动态注入生命周期的钩子的,需要保证钩子注入的是当前执行 setupvm。所以在执行这个 setup 函数时,需要保存当前执行的 vm

创建上下文

这里的上下文不是 vm,而是对 vm 提取了一些关键信息而成的 ctx

ctx 包含的 props 有这些:

const props: Array<string | [string, string]> = [
      'root',
      'parent',
      'refs',
      ['slots', 'scopedSlots'],
      'attrs',
    ];
const methodReturnVoid = ['emit'];
// vm.$root === ctx.root
// vm.$refs === ctx.refs
// ...
复制代码

创建出来的 ctx 对象作为 setup 函数的第二个参数传入。

当使用 Vue-router vuex 等插件,这里的上下文就会缺失 router store 等。

如果需要使用 vue-router,一个比较简单的方法是通过 ctx.root.$router 这样来使用。

在官方仓库中有一个相关的PR被Close掉了。

feat: add an option to bind other props #37

为了考虑与RFC一致,方便迁移,所以作者不添加除了RFC以外的API。

lifeCycle

lifecycle.ts

setup 执行的过程中,可以动态插入生命周期 钩子。这里生命周期的代码比较简单,主要需要拿到当前执行上下文的 vm,再插入一个 callback 到相应的生命周期中。

injectHookOption 里面有一个 merge Function 这个函数主要是 Vue 某个配置的合并策略,默认是简单的覆盖。具体文档

为什么这里没有 beforeCreate 的钩子,因为 beforeCreate 的钩子已经被使用了,所以能使用只能是 beforeCreate 之后生命周期的钩子。

watch

watch 支持3种模式pre post sync

pre mode: 在 rerender 之前执行回调

post mode: 在 rerender 之后执行回调

sync mode: 同步执行回调

watch 中主要有 2 个方法,createSingleSourceWatchercreateMuiltSourceWatcher

对应的是 2 种模式,一种是只 watch 一个数据源,一种是 watch 多个数据源。

watch 在 vm 上维护了 2 个队列,WatcherPreFlushQueueWatcherPostFlushQueue

这 2 个队列是用来维护所需要执行的回调。每一次经过 flush 后,队列会被清空。

installWatchEnv

installWatchEnv 方法中,对当前 vm 的进行初始化队列和绑定生命周期的事件。

注: 在Vue生命周期的 callHook 调用时,会 emit 出相应的事件 Hook Event

flushWatcherCallback

该方法用来维护 watch 的回调队列。

会根据 mode 选择插入回调的队列或者立即执行。

方法中有一个函数 fallbackFlush 用于当所 watch 的值发生了变化,但是没有触发 Vue 的 update 时进行一个兜底的 flush。例如在 setup 函数中改变某个 watch 的值。

其他发生 update 的变更,会交给生命周期的 event 去执行队列的回调。

vm.$on('hook:beforeUpdate', () => flushQueue(vm, WatcherPreFlushQueueKey));
vm.$on('hook:updated', () => flushQueue(vm, WatcherPostFlushQueueKey));
复制代码
createSingleSourceWatcher

在这个方法里,value 会被转换成 getter 的形式。

  if (isWrapper<T>(source)) {
    getter = () => source.value;
  } else {
    getter = source as () => T;
  }
复制代码

在 RFC 中,有这样的一句话**Unlike 2.x $watch, the callback will be called once when the watcher is first created.**所有 watch 的回调在创建的时候会先执行一遍,除非他是 lazy 的,也就是说,watch 默认是 immediate 的。如果是 lazy 的,会将回调放到 2 个队列的其中一个。

除了第一次执行回调的时机比较特殊,其余的回调执行时机都交给 flushWatcherCallback

createMuiltSourceWatcher

该方法用来创建对多个数据源的 watch

同样的在所有数据源初始化结束完成后会立即执行一次回调函数,除非是 lazy 的。

多个数据源与单个数据源不同的是,多个数据源需要维护多个数据源的 newValueoldValue。以及 stop 函数,需要对所有 watch 的数据源 stop

function execCallback() {
    cb.apply(
      vm,
      watcherContext.reduce<[T[], T[]]>(
        (acc, ctx) => {
          acc[0].push((ctx.value === initValue ? ctx.getter() : ctx.value) as T);
          acc[1].push((ctx.oldValue === initValue ? undefined : ctx.oldValue) as T);
          return acc;
        },
        [[], []]
      )
    );
  }

function stop() {
    watcherContext.forEach(ctx => ctx.watcherStopHandle());
  }
复制代码

多个数据源的回调参数为2个数组,newValuesoldValues 顺序与所观察的数据源的顺序一一对应。

总结

以上是阅读源码的一个小笔记。Vue Function API 的源码不长,所以推荐大家还是可以去阅读一下源码。

在阅读源码的过程中见识到了很多以前没有接触过,不知道的 API,比如 Vue 中 callHook 的事件。可以利用 vm 做很多事情。以及对逻辑的抽离和复用有了更深的思考,是否有了 Function API 就已经不需要 vuex。一起期待 Vue 3.0 吧,完整的类型会让整个开发体验完全不一样。

参考

  1. Vue 插件
  2. vm-watch
  3. Vue 3.0 RFC
  4. Vue Function API Repository