之前关于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
方法的执行,从而更新渲染。