Vue3.0 的 RFC 已经发布了几个月了,Vue 底层几乎没有变更,仍是沿用原来响应式的。因此一直在思考能不能使用如今的版本,实现 RFC 中的 API,直到看到了 Vue Function API 这个库,这个库让开发者提早尝鲜到了RFC 中的 API,固然做为 RFC,因此最终 3.0 的 API 仍是未知的,以及底层的实现也还未知。html
Dreamacro大佬说 类型才是 functional 的最大意义。我以为很是有道理,在这个 ts 盛行的年代。vue
想复用逻辑和状态,关键在于如何建立一个能够被 Vue 观察的对象(响应式对象)。当响应式的对象发生了变化时,Vue 会开始它的更新逻辑,至于它是怎么更新了,这里不做讨论。其次就是,怎么将这个状态绑定到 vm
上,除了使用 computed
来手动绑定以外,还能够用什么方法。git
在 Vue 2.6 以前,想建立一个响应式对象须要实例化一个 Vue
,但在 Vue 2.6 以后,能够经过Vue.observable
来建立一个响应式的对象。github
Vue 2.6 之前vue-router
const create = (obj: object) => {
const vm = new Vue({ data: () => obj })
return vm.$data
}
复制代码
Vue 2.6 以后vuex
const create = (obj: object) => Vue.observable(obj)
复制代码
这里有一个 DEMO,能够看出,普通的对象更新是不会触发 Vue 的更新逻辑的。响应式的对象,即便不在该 Vue 实例中去更改值,也会触发 Vue 的更新,能够在 DEMO 的控制台中尝试一下输入 x.value++
typescript
DEMO 中,不处理上下文,无论任何生命周期,只想表达 setup
以及 value
是如何工做的。api
首先考虑一下这个 value
,当这个 value
的类型为 number
或者 string
等非对象的类型时,为了建立一个响应式的对象,因此须要一层 wrapper
,这样 Vue
才能建立一个响应式对象。这就是为何 RFC 中使用 value
是 variable.value
的形式了。数组
可是若是 value
自己是一个对象的话,能够不须要这层 wrapper
。可能为了统一,因此都加上了这层。app
其次 setup
做为一个 Vue
的配置,须要在 Vue 实例化的时候执行的,选择 Vue 的第一个生命周期 beforeCreate
钩子中执行这个函数,等于用 setup
函数来替代 beforeCreate
,这样能够在 setup
函数中使用其余生命周期的钩子。
最后是这个 setup
的返回值,如何 unwrapped
并将值挂到 this
上提供给 template
使用。
这里提供一个最简单的 DEMO 能够看 vfp.js
的实现,仅仅在 beforeCreate
执行了一下 setup
并将返回值作一层 unwrapped
并挂载到 vm
上 提供给 template
使用。
注: vm => Vue实例
先来看几个关于 wrapper
的类。
这个抽象类主要实现了一个 setVmProperty
的方法,主要用来将 value
这个挂载到 vm
上。
这个类就是 value
函数实际使用的类,继承 AbstractWrapper
,主要实现了 value get
和 value set
而且约定使用 $$state
做为响应式对象中的 key
这个类主要用于对 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
也可使用其余的。
value
和 state
函数都比较简单,目的就是建立响应式对象。
value
与 state
的区别在于,value
方法能够将一个非对象的类型(number 、 string 、 boolean),包装成一个响应式对象。而 state
能够直接将一个非响应式对象包装成一个响应式对象。
const A = state({ value: 0 })
const B = value(0)
// 这二者在某种意义上是等价的。
复制代码
主要有3个方法 functionApiInit
initSetup
createSetupContext
在 Vue 生命周期中的 beforeCreate
执行 functionApiInit
。主要用于判断是否在 Vue 的配置中存不存在 setup
方法,若是存在,就进行 initSetup
。
initSetup
方法中,先经过 createSetupContext
方法建立一个本身的上下文。而后经过一个全局变量,保存上一次的 vm
,再设置当前的 vm
,接下去将以前建立的 上下文以及props 做为参数执行 setup
函数。执行结束后,将当前的 vm
还原为上一次的 vm
。最后将 setup
的返回值,绑定到 vm
中。
这里为何须要保存以前所执行的上下文?issue
setup
是能够动态注入生命周期的钩子的,须要保证钩子注入的是当前执行 setup
的 vm
。因此在执行这个 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。
在 setup
执行的过程当中,能够动态插入生命周期
钩子。这里生命周期的代码比较简单,主要须要拿到当前执行上下文的 vm
,再插入一个 callback
到相应的生命周期中。
在 injectHookOption
里面有一个 merge Function
这个函数主要是 Vue 某个配置的合并策略,默认是简单的覆盖。具体文档
为何这里没有 beforeCreate
的钩子,由于 beforeCreate
的钩子已经被使用了,因此能使用只能是 beforeCreate
以后生命周期的钩子。
watch 支持3种模式pre
post
sync
pre mode
: 在 rerender 以前执行回调
post mode
: 在 rerender 以后执行回调
sync mode
: 同步执行回调
watch 中主要有 2 个方法,createSingleSourceWatcher
和 createMuiltSourceWatcher
对应的是 2 种模式,一种是只 watch 一个数据源,一种是 watch 多个数据源。
watch 在 vm 上维护了 2 个队列,WatcherPreFlushQueue
和 WatcherPostFlushQueue
这 2 个队列是用来维护所须要执行的回调。每一次通过 flush
后,队列会被清空。
installWatchEnv
方法中,对当前 vm
的进行初始化队列和绑定生命周期的事件。
注: 在Vue生命周期的 callHook
调用时,会 emit
出相应的事件 Hook Event
该方法用来维护 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));
复制代码
在这个方法里,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
。
该方法用来建立对多个数据源的 watch
。
一样的在全部数据源初始化结束完成后会当即执行一次回调函数,除非是 lazy
的。
多个数据源与单个数据源不一样的是,多个数据源须要维护多个数据源的 newValue
和 oldValue
。以及 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个数组,newValues
和 oldValues
顺序与所观察的数据源的顺序一一对应。
以上是阅读源码的一个小笔记。Vue Function API
的源码不长,因此推荐你们仍是能够去阅读一下源码。
在阅读源码的过程当中见识到了不少之前没有接触过,不知道的 API,好比 Vue 中 callHook 的事件。能够利用 vm
作不少事情。以及对逻辑的抽离和复用有了更深的思考,是否有了 Function API
就已经不须要 vuex
。一块儿期待 Vue 3.0
吧,完整的类型会让整个开发体验彻底不同。