首先,我们修改一下示例代码:
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
的方法多,比如有各种检查。
消化一下,下回继续~