watch 和 computed 的实现都基于 ReactiveEffect 类,首先讲解 watch 的实现原理。
watch 对应的官方 api 文档点击这里,watch api 的常见用法如下所示:
let obj = reactive({
text: 'Hello'
})
watch(() => obj.text, (newVal, oldVal) => {
console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
})
setTimeout(() => {
obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
}, 1000)
在上面的代码中,watch api 接受了两个参数,第一个参数是一个 getter 函数,这个函数读取了一个响应式的属性,第二个参数是一个回调函数,当 obj.text 属性发生变化的时候,会触发执行这个回调函数,参数是 newVal 和 oldVal。 我们以上面的代码为例,进行 watch 实现原理的讲解。
如果读懂了上一篇博客的话,watch 的实现原理是很容易想到了,我们可以利用 ReactiveEffect 类和 scheduler 函数实现 watch 的功能。
首先观察上面代码中的第一个参数,这个参数是一个函数,并且函数的执行进行了响应式数据的读取,我们可以把这个函数当成一个副作用函数,这个函数的执行能够触发响应式数据的依赖收集。watch api 第二个参数是一个回调函数,当读取的响应式数据发生了变化的话,要求触发执行这个回调函数。我们知道当响应式数据发生了变化的时候,在默认的情况下,Vue 会触发执行依赖的 run 方法,但是如果 ReactiveEffect 的实例上有 scheduler 函数的时候,Vue 则会触发执行这个 scheduler 函数,因此,我们可以在 scheduler 中进行回调函数的触发执行。
结合上面的思路,最简实现代码如下所示:
function watch(
source,
cb
): {
doWatch(source, cb)
}
function doWatch(
source,
cb
) {
let getter = source
let oldValue
let scheduler = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
const effect = new ReactiveEffect(getter, scheduler)
// init run
oldValue = effect.run()
}
watch 的第一个参数除了能是一个函数外,还可以是一个 reactive 对象,用法如下所示:
let obj = reactive({
text: 'Hello'
})
watch(obj, (newVal, oldVal) => {
console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
})
setTimeout(() => {
obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
}, 1000)
此时,watch 会监控整个 reactive 对象,当对象中有任何一个属性发生了变化的话,都会执行回调函数,这个的实现原理其实非常简单,只需要改写一下 getter 即可。代码如下所示:
function doWatch(
source,
cb
) {
let getter
if(isReactive(source)){
getter = () => traverse(source)
} else {
getter = source
}
let oldValue
let scheduler = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
const effect = new ReactiveEffect(getter, scheduler)
oldValue = effect.run()
}
// traverse 函数的作用是:遍历读取 value 中的属性
function traverse(value) {
if (!isObject(value)) {
return value
} else {
for (const key in value) {
traverse(value[key])
}
}
}
新增改写的 getter 等于 () => traverse(source),traverse 函数的作用是遍历读取对象中的所有属性,这样,响应式对象中所有属性的 dep 都会对当前的 activeEffect 进行依赖收集,进而也就达到了监控整个 reactive 对象的目的。
watch api 还支持其他的特性,这里就不细说了。
computed 的官方文档 api 点击这里,常见用法如下所示:
let obj = reactive({
text: 'Hello'
})
let computedData = computed(() => `${obj.text},xxx`)
effect(() => {
console.log("副作用函数执行")
console.log(computedData.value)
})
setTimeout(() => {
obj.text = 'Vue'
}, 1000)
// 输出如下所示:
// 副作用函数执行
// Hello,xxx
/ 1s后 //
// 副作用函数执行
// Vue,xxx
computed 函数的参数是一个 getter 函数,该 getter 函数会读取响应式数据,并返回一个值。computed 函数的返回值是一个 ref 值,我们可以在其他的 effect 中读取这个 ref 值,后续当 computed getter 所依赖的响应式数据发生变化时,会重新触发执行这些读取了 ref 值的 effect。
computed 有以下几大特征。
computed 的内部实现借助了 ReactiveEffect 类和访问器属性。这里,我直接给出实现的代码,然后进行代码讲解。
export class ComputedRefImpl {
public dep?: Dep = undefined
public readonly effect: ReactiveEffect
public _dirty = true
private _value!: T
public readonly __v_isRef = true
constructor(
getter: ComputedGetter
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
}
get value() {
trackRefValue(this)
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()!
}
return this._value
}
}
export function computed(
getter
) {
const cRef = new ComputedRefImpl(getter)
return cRef
}
我们发现 computed 函数很简单,主要实现代码在 ComputedRefImpl 类中,首先讲解其内部几个属性的作用。
上面四个核心属性如果理解了的话,整个 comuted 的实现原理也就基本都通了。
我们先看 constructor 函数,在构造器函数中,我们 new 了一个 ReactiveEffect 类的实例,第一个参数就是我们计算属性的 getter,当我们执行 effect.run() 的时候,计算属性就会进行值的重新计算,新值就是 run 函数的返回值。第二个参数是一个 scheduler 函数,当 getter 函数依赖的响应式数据发生变化的时候,这个调度函数就会被触发执行,在这里要做的事情是将 _dirty 属性设为 true,这标识着当前的计算属性需要重新计算求值,然后调用 triggerRefValue(this),这个函数的作用是重新执行依赖了当前响应式数据的副作用函数。
当我们执行依赖了计算属性值的副作用函数时,副作用函数会进行计算属性值的读取操作,也就是 computedDate.value,这个值是 ComputedRefImpl 类的一个访问器属性,这会触发类内部的 get value() 函数,这个函数的作用是返回最新的计算属性值,其内部首先进行计算属性值的依赖收集,然后判断 _dirty 是不是 true,如果为 true 的话,说明当前内部缓存的计算属性值不是最新的,需要执行 effect.run() 进行重新求值,计算出的最新值存放到 this._value 属性上,函数的最后 return this._value 即可。
ok,Vue3 的 computed 和 watch 讲完了,下一篇博客,讲讲 Vue2 中的 computed 和 watch。