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。