export function ref<T extends object>(
raw: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(raw: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(raw?: unknown) {
if (isRef(raw)) {
return raw
}
// RefKey => composition-api.refKey
// ref 函数会构造一个 obj
/**
* {
* 'composition-api.refKey': val
* }
*/
// 首先创建一个响应式的数据,以 RefKey 作为 key
const value = reactive({ [RefKey]: raw })
// 创建一个 ref 的代理,传入自定义的 getter / setter 函数,内部实际访问的是这个响应式数据的值
return createRef({
get: () => value[RefKey] as any,
set: (v) => ((value[RefKey] as any) = v),
})
}
export function createRef<T>(options: RefOption<T>, readonly = false) {
// 使用 RefImpl 创建一个 ref 实例
const r = new RefImpl<T>(options)
// seal the ref, this could prevent ref from being observed
// It's safe to seal the ref, since we really shouldn't extend it.
// related issues: #79
const sealed = Object.seal(r)
readonlySet.set(sealed, true)
return sealed
}
class RefImpl<T> implements Ref<T> {
readonly [_refBrand]!: true
public value!: T
constructor({ get, set }: RefOption<T>) {
// 对这个 ref 类型的值定义一个 value 键名的代理,通过 get / set 函数,这样也就和响应式系统给关联上了
// 所以 compositable api 对于 ref 类型值的访问需要通过 value 属性
proxy(this, 'value', {
get,
set,
})
}
}
Composition Api
作为一个单独的插件被挂载是Vue
根的构造函数上。setup 初始化函数
插件混入了全局的钩子函数,在
beforeCreated
执行的阶段完成一些 composition api 初始化的操作。在 vue 实例的生命周期执行的时机为:
initProps
->initMethods
->initData
。其中Composition Api
对于 vue 实例的data
属性做了一层代理,确保setup
函数的调用是在data
初始化之前。响应式系统
在
Vue
在整个设计当中,响应式系统作为联系数据与模板的桥梁。Composition Api
使用的是一套独立的响应式系统,这套系统可以脱离 vue 的环境来被使用。Reactive
reactive
作为一个独立的响应式 api 被暴露出来。在
reactive
api 的内部实现当中:其实可以看到
reactive
内部首先通过Vue.observable
api 完成数据的响应式的处理。然后有一个非常重要的操作:setupAccessControl
,主要就是对于经过Vue.observable
处理后的数据重置原有的代理实现,主要是为了兼容ref
数据类型(对于 ref 数据的访问/改写都通过 value.value 属性进行操作,即自动的unwrap
自动展开,如果不是ref
数据类型的话,那么还是和原有的访问保持一致)。Ref
如何理解通过
ref
构造的数据:由于基本数据类型特性的原因,如果需要变成可响应式的数据,那么是需要通过一层代理,将基本数据类型转化为
object
,然后再将这个object
转化为响应式的数据(还是通过reative
来完成数据的改造),之后的数据变更都通过这个object
来完成操作。在@vue/composition-api
内部实现当中,是直接构造了一个普通对象:这个普通对象经由
reactive
api 的处理转化为了响应式的数据。因此通过访问object[RefKey]
上的 getter/setter 函数便可完成响应式数据的收集与通知等操作。虽然对于 Primitive Value 已经构造出来了响应数据,但是这个时候还不够,因为这个响应式数据的 key 是composition-api.refKey
,肯定不能通过这个 key 去直接访问。因此针对这个问题,再一次的对这个响应式数据做了一层代理,即创建一个RefImpl
实例,这个实例上的value
属性可以访问到响应式数据。通过
ref
构造的数据是RefImpl
类的实例对象,这个实例对象具有唯一可供遍历的键名:value
。因此在ref
作为reactive
对象的属性进行访问或修改的时候会自动unwrap
对应的value
值,因此不再需要单独通过.value
的写法去访问:其他的一些 API
computed
在
computed
的实现里面,首先是获取当前正在访问的组件实例,并完成 getter/setter 的初始化工作。需要注意的是computed
方法返回的数据类型为 ref。这里也可以回忆下,对于将 plain object 类型的数据变为响应式的是通过reactive
来完成的,最终得到的数据也可以直接访问原有数据上的 key 来获取对应的值。但是对于 Primitive value 使用 ref 或者是对于使用 computed 构造出的一个新的变量值,其实这两者都会遇到一个获取这个构造之后的响应式数据值的问题,因为对于 plain object 来说,直接访问对应的 key 就好了,但是这2者的数据类型最终都为封装为:{ [RefKey]: raw }
,所以最终会给这个不太方便访问实际值的 plain object 创建一个新的value
属性以供访问。effectScope
专门提供了一个API用以 effect 的收集工作并提供了相关的API对于 effect 做统一的管理。事实上在我们使用 composition-api 的时候,都是需要一个 vue 实例作为载体的(不管这个 vue 实例是作为实际的需要被其他组件使用的组件还是说仅仅是在 vue 的生态里面使用这些能力)。
就拿我们熟知的和渲染有关的 render watcher,以及 computed 这些借助 Watcher。一旦这些 watcher 获取值的过程中就会就会进行响应式数据的依赖关系绑定。在 Vue2 当中这些 watcher 都是和组件都非常强的绑定关系的,一旦组件被销毁,那么对应的依赖关系也会被清除掉。然而在 composition-api 当中,你可以在任何地方使用响应式的 api,但是这些响应式的 api 的依赖关系和副作用却不是那么方便的进行管理。
所以官方提供了 EffectScope 这个更加抽象的 api 来对 vue 生态当中的响应式数据和所带来的副作用进行统一的管理。我们通过分析
@vue/composition-api
相关的代码也会发现一个 vue 实例的创建也会伴随着 effectScope 的创建。在组件销毁的时候会自动触发
scope.stop()
来消除副作用之前的依赖关系(在bindCurrentScopeVM
完成 vm 实例和 scope 绑定的时候就完成了 destory 生命周期的注册)。此外还可以看到的就是在一个组件的生命周期当中,内部是通过调用
new EffectImpl(vm) as EffectScope
来完成 scope 的初始化的工作,这里接受的参数就是当前的组件实例。但是对于我们不是在一个组件的生命周期当中去使用响应式api的时候是调用new EffectScope()
这个时候不需要接受参数,而是内部会创建一个全新的 vue 实例并完成 scope 的初始化以及和 vue 实例绑定的过程。当我们需要清除这部分响应式数据的依赖关系的时候,直接调用
scope.stop
,其实内部就会调用绑定的 vm 实例的destory
方法完成组件的清除工作。watch & watchEffect
首先看下 watchEffect:
生命周期
生命周期的钩子函数只能在
setup
内同步的调用。因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。因为
Composition Api
设计的规范:在 setup 函数里面去调用生命周期函数,然后在组件的对应生命周期的节点去触发这些函数。因此onMounted
钩子函数内部执行的时候(同步调用),在全局当中找到当前正在调用的 vm 组件实例。然后完成mounted
钩子的挂载(即对应的vm.$options
配置)。这里需要注意的是
setup
函数执行的时机是在initData
之前,在组件的生命周期里面,这是一个非常靠前的时间节点(因为很多生命周期函数都还没有调用),那么这些生命周期的 hooks 所要完成的工作就是收集在setup
里面调用的这些 lifecycle hook 的 callback。因为 Vue 里面提供了一个有关 options 合并的策略函数,具体参见core/utils/options.js
:其实就是完成 lifecycle callback 合并到一个数组的工作。然后在这个组件进行到实际的生命周期的时候就会执行之前已经收集好的 callback。