之前关于Vue3的整体运行过程做了简单的原理分析,在实际开发过程中使用Vue3的API也是必不可少的,接下来会对Vue3的一下常用的Composition API的实现原理简易分析
computed的使用
import { computed, reactive } from 'vue'
// 传参是函数
const double = computed(() => num.count * 2)
// 传参是对象,包含set、get钩子函数
const double = computed({
get: () => {
console.log('get double')
return num.count * 2
},
set: (val) => (num.count = val / 2)
})
double.value = 20 // 直接修改double的值
当参数为函数时,计算属性double的值随着num.count的值的变化而变化,但是计算属性的值不可直接修改;当参数为对象时,计算属性的值可读可写。
computed API内部实现
首先当执行setup函数时,遇到computed函数开始执行
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter)
if (__DEV__ && debugOptions) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
校验参数
computed函数内部首先校验入参getterOrOptions是否是一个函数,如果是函数,则会设置setter为() => console.warn(),当修改计算属性的值时会报错;如果是一个对象,也只支持对象的属性为set、get。
ComputedRefImpl实例对象
之后创建ComputedRefImpl实例对象,参数为set、get 和 是否是只读属性的标识位。接着看下ComputedRefImpl类的内部实现
// ComputedRefImpl 类
public dep?: Dep = undefined // 依赖
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
// constructor
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = new ReactiveEffect(getter, )
this[ReactiveFlags.IS_READONLY] = isReadonly
}
首先给ComputedRefImpl类的effect属性设置为ReactiveEffect类的实例对象,传参为getter钩子函数 和 自定义的调度函数scheduler。之后定义value属性的get和set拦截钩子函数。从这里可以看出使用计算属性的时候也是依赖于它的value属性(template模版中可直接使用,因为计算属性的__v_isRef=true,参考ref函数的使用与实现)
触发计算属性的get函数
在setup解析完成之后,就是模版编译生成render函数,之后执行render函数,当遇到计算属性时,会获取它的值(类似于获取ref返回的数据值)。触发get钩子函数的执行
get value() {
const self = toRaw(this)
trackRefValue(self)
if (self._dirty) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
在get钩子函数中首先是trackRefValue方法进行依赖收集,大致实现是在ComputedRefImpl实例对象的dep属性中添加全局的依赖activeEffect。
因为在模版编译之前会创建ReactiveEffect实例对象,参数的fn为componentUpdateFn(render+patch),componentUpdateFn方法的执行是因为执行了ReactiveEffect实例对象中的run方法,run方法中会将activeEffect全局依赖赋值当前的ReactiveEffect对象。所以在ComputedRefImpl的value的get钩子函数执行时,activeEffect全局依赖为ReactiveEffect实例对象(render + patch)
这样计算属性的dep中收集的是(render+patch)的依赖。
get钩子函数中后续执行了self.effect.run()!,effect属性是初始化computed对象时创建的ReactiveEffect实例对象,执行run方法,首先将全局的activeEffect依赖设置为计算属性的ReactiveEffect对象,然后执行传入的fn,即调用computed方法时传入的get函数(() => num.count * 2)。
当获取num.count时,因为num是Proxy代理对象,所以会触发Proxy拦截器的get钩子函数,进行num的依赖收集,此时是将全局依赖activeEffect收集在了num的dep属性中。
依赖数据发生变化
当依赖的num数据发生变化时会触发依赖的执行,也就是计算属性创建的ReactiveEffect实例对象,执行传入的调度函数
// 计算属性传入的调度函数scheduler
() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
}
在计算属性的调度函数中又会执行自己收集的依赖,而计算属性收集的依赖是模版编译后创建的ReactiveEffect实例对象(render+patch),此时执行调度函数则是重新执行render和patch方法,更新页面渲染。
setter钩子函数
set value(newValue: T) {
this._setter(newValue)
}
ComputedRefImpl实例对象的setter钩子函数,就是执行传入的set属性函数,所以当入参是对象是,修改计算属性的值会相应的执行传入的set函数。
总结
首先computed执行时会创建ReactiveEffect实例对象,传参为传入的get和自定义的调度函数scheduler。然后设置value属性的get、set拦截函数。当获取计算属性时触发get函数的执行,会收集render+patch的依赖,然后执行计算属性值的计算过程,当获取依赖数据的值时会触发依赖数据的get钩子函数执行,收集依赖,收集的是计算属性的ReactiveEffect对象。
当依赖数据发生变化时,首先会触发依赖数据的依赖执行,就是计算属性中自定义的调度函数,执行中又会执行计算属性的依赖,就是render和patch方法的执行,从而更新渲染。