Libon

《Vue3 响应式系统原理》(下)

响应式系统 activeEffect & ref & computed

我们继续。在上一章的结尾中,我们的代码变成了大致这样子

const product = reactive({ price: 5, quantity: 2 })
let total = 0

function effect() {
  total = product.price * product.quantity
}

effect()

console.log(`total is ${total}`) // total is 1

product.price = 10

console.log(`total is ${total}`) // total is 20

如果我们以另一种形式添加 get 依赖,比如 production.quantity 的时候,我们的代码将会调用追踪函数:

console.log('Update quantity to = ', product.quantity)

它将会触发 track(product, ‘quantity’) 函数,会遍历 targetMap,还有 deps,以确保当前的 effect 会被保存下来,但这不是我们想要的, 我们应该只在 effect 里调用 track 函数,所以我们需要修改一下代码以适配这个场景。为此,我们引入一个新的变量:activeEffect,它用于保存当前正在执行的 effect 函数。

activeEffect

我们将重新创建一个 effect 函数,用它来实现这个逻辑。

let activeEffect = null

function effect(effectFn) {
	activeEffect = effectFn
	activeEffect()
	activeEffect = null
}

同时将原本的 effect 函数更新:

- function effect() {
-   total = product.price * product.quantity
- }

+ effect(() => {
+   total = product.price * product.quantity
+ })

当然,这项改动也意味着我们不再需要调用 effect 函数,因为它会在我们传递函数的时候调用。

+ let activeEffect = null

+ function effect(effectFn) {
+ 	activeEffect = effectFn
+ 	activeEffect()
+ 	activeEffect = null
+ }

const product = reactive({ price: 5, quantity: 2 })
let total = 0

- function effect() {
-  total = product.price * product.quantity
- }
+ effect(() => {
+   total = product.price * product.quantity
+ })

- effect()

console.log(`total is ${total}`) // total is 1

product.price = 10

console.log(`total is ${total}`) // total is 20

但是我们现在需要更新追踪函数,那这个时候 activeEffect 变量就会派上用场了。回到 track() 函数上:

function track(target, key) {
	let depsMap = targetMap.get(target)

	// 如果这个对象没有被观察则将它添加到依赖列表中
	if (!depsMap) {
		targetMap.set(target, (depsMap = new Map()))
	}

	// 读取对象上的子属性
	let deps = depsMap.get(key)
	if (!deps) {
		depsMap.set(key, (deps = new Set()))
	}

	deps.add(effect)
}

// 我们需要执行两个操作
// 1. 判断当前是否存在 activeEffect
// 2. 将 activeEffect 替换原本的 effect
function track(target, key) {
	// 如果不存在正在执行的 activeEffect 则不收集此次访问的依赖
	if (!activeEffect) return

	let depsMap = targetMap.get(target)

	// 如果这个对象没有被观察则将它添加到依赖列表中
	if (!depsMap) {
		targetMap.set(target, (depsMap = new Map()))
	}

	// 读取对象上的子属性
	let deps = depsMap.get(key)
	if (!deps) {
		depsMap.set(key, (deps = new Set()))
	}

	// 如果是以 effect 类型收集的响应式依赖则将它放置到依赖列表中
	deps.add(activeEffect)
}

我们用一个比较高级一点的测试用例来验证这个过程。

const targetMap = new WeakMap()
const product = reactive({ price: 5, quantity: 2 })
let total = 0

let activeEffect = null

function effect(effectFn) {
	activeEffect = effectFn
	activeEffect()
	activeEffect = null
}

function track(target, key) {
  if (!activeEffect) return

	let depsMap = targetMap.get(target)

	// 如果这个对象没有被观察则将它添加到依赖列表中
	if (!depsMap) {
		targetMap.set(target, (depsMap = new Map()))
	}

	// 读取对象上的子属性
	let deps = depsMap.get(key)
	if (!deps) {
		depsMap.set(key, (deps = new Set()))
	}

	deps.add(activeEffect)
}

function trigger(target, key) {
	const depsMap = targetMap.get(target)

	// 如果整个对象都没有被追踪则直接返回
	if (!depsMap) {
		return
	}

  let deps = depsMap.get(key)
  if (!deps) {
    return
  }

  deps.forEach(effect => effect())
}

let salePrice = 0

effect(() => {
  total = product.price * product.quantity
})

effect(() => {
  salePrice = product.price * 0.9
})

function reactive(target) {
	const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key) // 设置响应式依赖项
			return result
		},
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      const result = Reflect.set(target, key, value, receiver)
      // 当值发生变化的时候通知所有依赖项
      if (oldValue !== result) {
        trigger(target, key)
      }
			return result
		}
  }

	return new Proxy(target, handler)
}

console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)

product.quantity = 3

console.log(`Before updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)

product.price = 10

console.log(`Before updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`)

通过以上代码我们可以确定现在代码不会再没有 activeEffect 函数的时候执行追踪函数。

ref 的实现

但是光这么做意义还不是很大,为什么我们没有根据销售价格来计算总数呢?比如将 total = product.price * product.quantity 替换为 total = salePrice * product.quantity 。但这并不能正常工作,因为 salePrice 不是响应式的,所以这是使用 ref 的好时机。 ref 接收一个值,并返回一个响应的,可变的 Ref 对象。 Ref 对象只有一个 .value 属性,它指向内部的值,这听起来好像是从一个文件中复制粘贴出来的(hhh)。那我们的代码将会变成这样:

let salePrice = ref(0)

effect(() => {
  total = salePrice.value * product.quantity
})

effect(() => {
  salePrice.value = product.price * 0.9
})

现在我们需要考虑如何定义及实现 ref 。第一种方法我们可以使用上面的 reactive 函数来实现它:

function ref(initialValue) {
	return reactive({ value: initialValue })
}

但是 Vue3 中并不是这么做的,因为 reactive 返回的东西可能会包含其他属性,而 ref 应该只包含值。为了学习 Vue3 如何实现 ref,我们需要了解 对象访问器(getter) 是什么,有时也被称之为 计算属性。但这里指的并不是 Vue3 中的计算属性,而是 JavaScript 中的计算属性。我们先看个例子:

const user = {
	firstName: 'Gregg',
	lastName: 'Pollck',

	get fullName() [
		return `${this.firstName} ${this.lastName}`
	},

	set fullName(value) {
		[this.firstName, this.lastName] = value.split(' ')
  }
}

console.log(`Name is ${user.fullName}`) // Gregg Pollck
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`) // Adam Jahr

在具备上述知识后,我们去尝试实现它:

function ref(raw) {
	const result = {
		get value() {
			// 去追踪这个 result.value 变量
      track(result, 'value')
      return raw
    },
    set value(newValue) {
      // 值发生变化时需要去触发所有的依赖项
      if (raw !== newValue) {
        raw = newValue
        trigger(result, 'value')
      }
    }
  }

  return result
}

我们尝试去使用它:

const product = reactive({ price: 5, quantity: 2 })
let salePrice = 0
let total = 0

effect(() => {
  total = salePrice.value * product.quantity
})

effect(() => {
  salePrice.value = product.price * 0.9
})

console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)

product.quantity = 3

console.log(`Before updated total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice}`)

product.price = 10

console.log(`Before updated total (should be 27) = ${total} salePrice (should be 9) = ${salePrice}`)

computed

其实到了这里,你应该早就想到了,为什么我不直接使用 computed 而是使用 effect 来每次都手动维护变量的值呢?说干就干,我们尝试着修改代码:

const product = reactive({ price: 5, quantity: 2 })
- let salePrice = 0
- let total = 0

- effect(() => {
-   total = salePrice.value * product.quantity
- })

- effect(() => {
-   salePrice.value = product.price * 0.9
- })

+ const salePrice = computed(() => {
+   return product.price * 0.9
+ })

+ const total = computed(() => {
+   return salePrice.value * product.quantity
+ })

那么我们尝试去实现之前,我们要知道我们需要做哪些事情:

  1. 创建一个响应式引用,我们可以叫它 result
  2. effect 中运行 getter ,因为我们需要监听响应值,然后将 getter 赋值于 result.value
  3. 最后我们返回 result

好的,那么我们去实现它:

function computed(getter) {
	let result = ref()

	effect(() => (result.value = getter()))

	return result
}

是的,就这么简单,我们将其应用起来,其最终结果应该与之前一致的,只不过在使用的时候需要增加 .value 属性,就像这样:

console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)

product.quantity = 3

console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)

product.price = 10

console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)

当然得意于 Proxy 的能力,使得我们可以做到在 Vue2 中做不到的事情:为一个没有声明初始值的变量添加响应式。比如说这样:

product.name = 'Shoes'

effect(() => {
  console.log(`Product name is now ${product.name}`)
})

product.name = 'Boots'

完整代码

截止到这里,完整代码如下:

let activeEffect = null

function effect(effectFn) {
	activeEffect = effectFn
	activeEffect()
	activeEffect = null
}

function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)

	// 如果这个对象没有被观察则将它添加到依赖列表中
	if (!depsMap) {
		targetMap.set(target, (depsMap = new Map()))
	}

	// 读取对象上的子属性
  let deps = depsMap.get(key)

	if (!deps) {
		depsMap.set(key, (deps = new Set()))
  }

	deps.add(activeEffect)
}

function trigger(target, key) {
	const depsMap = targetMap.get(target)

	// 如果整个对象都没有被追踪则直接返回
	if (!depsMap) {
		return
	}

  let deps = depsMap.get(key)
  if (!deps) {
    return
  }

  deps.forEach(effect => effect())
}

function ref(raw) {
	const result = {
		get value() {
			// 去追踪这个 result.value 变量
      track(result, 'value')
      return raw
    },
    set value(newValue) {
      // 值发生变化时需要去触发所有的依赖项
      if (raw !== newValue) {
        raw = newValue
        trigger(result, 'value')
      }
    }
  }

  return result
}

function computed(getter) {
	let result = ref()

	effect(() => (result.value = getter()))

	return result
}

const targetMap = new WeakMap()
const product = reactive({ price: 5, quantity: 2 })
const salePrice = computed(() => {
  return product.price * 0.9
})

const total = computed(() => {
  return salePrice.value * product.quantity
})

function reactive(target) {
	const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key) // 设置响应式依赖项
			return result
		},
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      const result = Reflect.set(target, key, value, receiver)
      // 当值发生变化的时候通知所有依赖项
      if (oldValue !== result) {
        trigger(target, key)
      }
			return result
		}
  }

	return new Proxy(target, handler)
}

console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)

product.quantity = 3

console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)

product.price = 10

console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)

product.name = 'Shoes'

effect(() => {
  console.log(`Product name is now ${product.name}`)
})

product.name = 'Boots'

以上。

cd ../