首先,我们修改一下示例代码:
let obj = reactive({a: 10, b: 20})
let timesA = 0
let effect = () => { timesA = obj.a * 10 }
effect()
console.log(timesA) // 100
obj.a = 100
// 新增一行,使用到 obj.a
console.log(obj.a)
console.log(timesA) // 1000由上节知识可以知道,当 effect 执行时我们访问到了 obj.a,因此会触发 track 收集该依赖 effect。同理,console.log(obj.a) 这一行也同样触发了 track,但 console.log 并不是响应式代码,我们预期不触发 track。
我们想要的是只有在 effect 中的代码才触发 track。
能想到怎么来实现吗?
首先,我们定义一个变量 shouldTrack,暂且 认为它表示是否需要执行 track,我们修改 track 代码,只需要增加一层判断条件,如下:
const targetMap = new WeakMap();
let shouldTrack = null
function track(target, key) {
if(shouldTrack){
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if(!dep) {
depsMap.set(key, dep = new Set());
}
// 这里的 effect 为使用时定义的 effect
// shouldTrack 时应该把对应的 effect 传进来
dep.add(effect)
// 如果有多个就手写多个
// dep.add(effect1)
// ...
}
}现在我们需要解决的就是 shouldTrack 赋值问题,当有需要响应式变动的地方,我们就定义一个 effect 并赋值给 shouldTrack,然后 effect 执行完后重置 shouldTrack 为 null,这样结合刚才修改的 track 函数就解决了这个问题,思路如下:
let shouldTrack = null
// 这里省略 track trigger reactive 代码
...
let obj = reactive({a: 10, b: 20})
let timesA = 0
let effect = () => { timesA = obj.a * 10 }
shouldTrack = effect // (*)
effect()
shouldTrack = null // (*)
console.log(timesA) // 100
obj.a = 100
console.log(obj.a)
console.log(timesA) // 1000此时,执行到 console.log(obj.a) 时,由于 shouldTrack 值为 null,所以并不会执行 track,完美。
完美了吗?显然不是,当有很多的 effect 时,你的代码会变成下面这样:
let effect1 = () => { timesA = obj.a * 10 }
shouldTrack = effect1 // (*)
effect1()
shouldTrack = null // (*)
let effect2 = () => { timesB = obj.a * 10 }
shouldTrack = effect2 // (*)
effect2()
shouldTrack = null // (*)我们来优化一下这个问题,为了和 Vue 3 保持一致,这里我们修改 shouldTrack 为 activeEffect,现在它 表示当前运行的 effect。
我们把这段重复使用的代码封装成函数,如下:
let activeEffect = null
// 这里省略 track trigger reactive 代码
...
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}同时我们还需要修改一下 track 函数:
function track(target, key) {
if(activeEffect){
...
// 这里不用再根据条件手动添加不同的 effect 了!
dep.add(activeEffect)
}
}那么现在的使用方法就变成了:
const targetMap = new WeakMap();
let activeEffect = null
function effect (eff) { ... }
function track() { ...}
function trigger() { ...}
function reactive() { ...}
let obj = reactive({a: 10, b: 20})
let timesA = 0
let timesB = 0
effect(() => { timesA = obj.a * 10 })
effect(() => { timesB = obj.b * 10 })
console.log(timesA) // 100
obj.a = 100
console.log(obj.a)
console.log(timesA) // 1000现在新建一个文件 reactive.ts,内容就是当前实现的完整响应式代码:
const targetMap = new WeakMap();
let activeEffect = null
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
function track(target, key) {
if(activeEffect){
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if(!dep) {
depsMap.set(key, dep = new Set());
}
dep.add(activeEffect)
}
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if(depsMap){
let dep = depsMap.get(key)
if(dep) {
dep.forEach(effect => effect())
}
}
}
const reactiveHandler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key)
return result
},
set(target, key, value, receiver) {
const oldVal = target[key]
const result = Reflect.set(target, key, value, receiver)
if(oldVal !== result){
trigger(target, key)
}
return result
}
}
function reactive(target) {
return new Proxy(target, reactiveHandler)
}现在我们已经解决了非响应式代码也触发 track 的问题,同时也解决了上节中留下的问题:track 函数中的 effect 只能手动添加。
接下来我们解决上节中留下的另一个问题:reactive 现在只能作用于对象,基本类型变量怎么处理?
修改 demo.js 代码如下:
import {effect, reactive} from "./reactive"
let obj = reactive({a: 10, b: 20})
let timesA = 0
let sum = 0
effect(() => { timesA = obj.a * 10 })
effect(() => { sum = timesA + obj.b })
obj.a = 100
console.log(sum) // 期望: 1020这段代码并不能实现预期效果,因为当 timesA 正常更新时,我们希望能更新 sum(即重新执行 () => { sum = timesA + obj.b }),但实际上由于 timesA 并不是一个响应式对象,没有收集它的依赖项,所以这一行代码并不会执行。
我们能否直接 let timesA = reactive(0) 来解决呢?答案是不行的,因为 reactive 只能接收一个对象作为参数。
那我们如何才能让这段代码正常工作呢?其实我们把基本类型变量包装成一个对象去调用 reactive 即可。
看过 Vue composition API 的同学可能知道,Vue 3 中用一个 ref 函数来实现把基本类型变量变成响应式对象,通过 .value 获取值,ref 返回的就是一个 reactive 对象。
实现这样的一个有 value 属性的对象有这两种方法:
- 直接给一个对象添加
value属性
function ref(intialValue) {
return reactive({
value: intialValue
})
}- 用
getter和setter来实现
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value)
}
}
return r
}现在我们的示例代码修改成:
import {effect, reactive} from "./reactive"
function ref(intialValue) {
return reactive({
value: intialValue
})
}
let obj = reactive({a: 10, b: 20})
let timesA = ref(0)
let sum = 0
effect(() => { timesA.value = obj.a * 10 })
effect(() => { sum = timesA.value + obj.b })
// 期望: timesA: 100 sum: 120 实际:timesA: 100 sum: 120
console.log(`timesA: ${timesA.value} sum: ${sum}`)
obj.a = 100
// 期望: timesA: 1000 sum: 1020 实际:timesA: 1000 sum: 1020
console.log(`timesA: ${timesA} sum: ${sum}`)增加了 ref 处理基本类型变量后,我们的示例代码运行结果符合预期了。至此我们已经解决了遗留问题:reactive 只能作用于对象,基本类型变量怎么处理?
Vue 3 中的 ref 是用 第二种 方法来实现的,现在我们整理一下代码,把 ref 放到 reactive.j 中。
const targetMap = new WeakMap();
let activeEffect = null
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
function track(target, key) {
if(activeEffect){
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if(!dep) {
depsMap.set(key, dep = new Set());
}
dep.add(activeEffect)
}
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if(depsMap){
let dep = depsMap.get(key)
if(dep) {
dep.forEach(effect => effect())
}
}
}
const reactiveHandler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key)
return result
},
set(target, key, value, receiver) {
const oldVal = target[key]
const result = Reflect.set(target, key, value, receiver)
if(oldVal !== result){
trigger(target, key)
}
return result
}
}
function reactive(target) {
return new Proxy(target, reactiveHandler)
}
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value)
}
}
return r
}有同学可能就要问了,为什么不直接用第一种方法实现 ref,而是选择了比较复杂的第二种方法呢?
主要有三方面原因:
- 根据定义,
ref应该只有一个公开的属性,即value,如果使用了reactive你可以给这个变量增加新的属性,这其实就破坏了ref的设计目的,它应该只用来包装一个内部的value而不应该作为一个通用的reactive对象; - Vue 3 中有一个
isRef函数,用来判断一个对象是ref对象而不是reactive对象,这种判断在很多场景都是非常有必要的; - 性能方面考虑,Vue 3 中的
reactive做的事情远比第二种实现ref的方法多,比如有各种检查。
消化一下,下回继续~