前言
了解过Vue3响应式的掘友们一定听说过Vue3的响应式是基于Proxy实现的,但是在说Proxy之前还需要了解一个概念:副作用函数。
响应式数据与副作用函数
副作用函数
我们先来了解一下副作用函数的定义:副作用函数指的是一个函数在执行过程中,除了返回值之外,还会对其外部环境产生一些可观察到的变化或影响。 这些变化既可以是改变外部变量,也可以是修改DOM。
例如下面这串代码:
<div id="app"></div>
let obj = {
name: "张三"
}
function effect() {
document.querySelector('#app').innerText = obj.name
}
在这段代码中我们可以看出,当调用effect函数时,它通过DOM操作修改了页面上的元素,也就是说这个函数对外部环境产生了影响(修改了DOM),因此这个函数产生了一个副作用。
相信大家到这里已经明白了副作用函数的概念,那么响应式数据又是什么,它和副作用函数之间又存在怎样的关系呢?
响应式数据
我们通常把:会影响视图变化的数据称为响应式数据。当响应式数据发生变化时,视图应当会发生变化。
结合上述代码,如果我们在读取obj.name的时候就将该副作用函数进行收集,后面对obj.name进行修改的时候将收集过来的副作用函数执行的话,不就实现响应式了吗。
思路是正确的,因此我们只需要拦截对象属性的读取和修改,就可以实现响应式了,但是很显然上述代码无法实现该功能,因为obj只是一个普通的对象。在ES2015之前,只能使用object.definePropertry实现,到了ES2015+中,我们就可以使用Proxy来实现对象属性的劫持了。
实现响应式数据
我们使用Proxy对一个对象进行代理,触发getter的时候将副作用函数存储起来,触发setter的时候将存储起来的副作用执行,下面是代码实现:
<div id="app"></div>
let obj = {
name: "张三"
}
let proxyObj = new Proxy(obj, {
get: (target, key, receiver) => {
const res = Reflect.get(target, key, receiver)
track()
return res
},
set: (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver)
trigger()
return result
}
})
function effect() {
document.querySelector('#app').innerText = proxyObj.name
}
effect()
上述代码中的track和trigger函数的功能分别是实现收集依赖和触发依赖,我们将在后面实现这两个函数的功能,在实现之前大家先思考两个问题:我们要怎么收集副作用函数?副作用函数要收集到哪里?
如何收集副作用函数
我们先来假设一下副作用函数存储在一个桶中,这个桶的名字叫bucket,当触发属性的getter时会调用track函数将副作用函数添加到桶里,触发setter时先更新原始数据,再将副作用函数从桶里取出来并重新执行,这样就实现响应式数据了,下面是实现该功能的伪代码:
function track() {
bucket.add(effect)
}
function trigger() {
bucket.forEach(fn => fn())
}
上述代码看上去已经实现功能了,实际上并不完善,大家可以想一个问题,如果副作用函数的名字不叫effect,那么这段代码是不是就不能正确工作了,我们希望的是副作用函数是一个匿名函数,也能被正确地收集到桶里。
因此我们需要提供一个注册依赖的机制,在调用effect时创建一个实例(该实例就是需要被收集的依赖),并传入一个副作用函数保存到该实例当中,再调用这个实例的run方法将该实例保存到一个activeEffect变量中,并调用传入的副作用函数触发属性的getter,将activeEffect收集起来,就可以完成依赖的收集了,下面用代码实现:
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
let activeEffect
class ReactiveEffect {
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
function track() {
bucket.add(activeEffect)
}
function trigger() {
bucket.forEach(effect => effect.run())
}
<div id="app"></div>
let obj = {
name: "张三"
}
let proxyObj = new Proxy(obj, {
get: (target, key, receiver) => {
const res = Reflect.get(target, key, receiver)
track()
return res
},
set: (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver)
trigger()
return result
}
})
// 先将依赖存储到一个activeEffect变量中,再执行副作用函数触发getter收集依赖
effect(() => {
document.querySelector('#app').innerText = proxyObj.name
})
// 触发setter重新执行依赖 修改视图
setTimeout(() => {
proxyObj.name = "李四"
}, 1000)
副作用函数收集到哪里
上文中提到,我们需要把依赖收集到桶里,那么这个桶具体是一个怎样的数据结构呢,这就是接下来我们需要探讨的问题。
假设桶只是一个单一的数据结构,我们通过effect函数触发name属性的getter,将依赖收集到桶里,1秒后为对象添加一个新的属性时,依赖中的副作用函数会再执行一次,这是我们不希望的。这是因为我们没有在副作用函数与被操作的目标字段之间建立明确的关系。例如无论读取了哪一个属性,都会将依赖收集到桶里,当设置哪一个属性,都会将桶里的依赖取出并执行,依赖和被操作字段之间没有明确的联系。
effect(() => {
console.log('run') // 打印2次
document.querySelector('#app').innerText = proxyObj.name
})
setTimeout(() => {
proxyObj.notExist = "test"
}, 1000)
因此我们需要设计桶的结构,使其不是一个单一的数据结构,这个结构需要在依赖和字段之间建立起联系,关系如下:
上图就是
桶的数据结构,其中WeakMap的键是被代理对象target,WeakMap的值是一个Map实例,而Map的键是被代理对象target的key,Map的值就是由依赖组成的Set。
为什么用WeakMap
简单地说,WeakMap对Key是弱引用,不影响垃圾回收器工作,因此一旦Key被垃圾回收器回收,那么对应的键和值就访问不到了。所以WeakMap经常用于存储那些只有当Key所引用的对象存在时才有价值的信息,例如在上面的场景中,如果target对象没有任何引用了,就说明用户不再需要它了,这时垃圾回收器会完成回收任务。但如果使用Map来代替WeakMap,那么即使用户的代码对target没有任何引用作用,这个target也不会被回收,最终可能导致内存溢出。
完善响应式系统
我们将桶的数据结构具体化之后,代码也可以完善起来了,下面是实现响应式数据的完整代码:
<div id="app"></div>
let obj = {
name: "张三"
}
let proxyObj = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
})
effect(() => {
document.querySelector('#app').innerText = proxyObj.name
})
setTimeout(() => {
proxyObj.name = "李四"
}, 1000)
const targetMap = new WeakMap()
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
let activeEffect
class ReactiveEffect {
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
// 收集依赖
function track(target, key) {
if(!activeEffect) return
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()))
}
trackEffects(dep)
}
function trackEffects(dep) {
dep.add(activeEffect)
}
// 触发依赖
function trigger(target, key) {
const depsMap = targetMap.get(target)
if(!depsMap) return
const dep = depsMap.get(key)
if(!dep) return
triggerEffects(dep)
}
function triggerEffects(dep) {
const effects = Array.isArray(dep) ? dep : [...dep]
for(const effect of effects) {
effect.run()
}
}
reactive和shallowReactive
reactive的实现其实已经和上述代码很接近了,其本质上就是返回一个代理对象,但这只是接近,并不是完整的reactive。说到reactive,那就不得不说shallowReactive了,这两个函数之间又有什么不一样呢,我们接着往下看。
浅响应和深响应
浅响应和深响应的区别,其实就是shallowReactive和reactive的区别,我们上面学的其实都是浅响应。
深响应
reactive本质上就是返回一个代理对象,如果我们把上面关于代理对象的代码作为reactive的返回值,就会得到以下代码:
function reactive(target) {
return createReact(target)
}
function createReact(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
})
}
我们再看下面的例子:
<body>
<div id="app"></div>
</body>
const obj = reactive({
age: {
number: 1
}
})
effect(() => {
document.querySelector('#app').innerText = obj.age.number
})
setTimeout(() => {
obj.age.number = 2
}, 1000);
这个例子中的age是一个嵌套对象,我们在1秒后对这个嵌套对象内的属性进行修改时,并没有重新触发依赖。这是因为当我们去读取obj.age.number时,首先要读取的是obj.age的值,由于通过Reflect.get得到obj.age的结果是一个普通对象,它并不是一个响应式对象,所以访问obj.age.number时时,是不能建立响应式联系的,所以要解决这个问题,我们需要对Reflect.get返回的结果做一层包装:
function reactive(target) {
return createReact(target)
}
function createReact(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
if(typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
})
}
如上面代码所示,当读取属性时,我们需要先监测该值是否为对象,如果是对象,则递归调用reactive函数将其包装成一个响应式数据并返回。当使用obj.age读取age属性时,得到的是一个响应式数据,因此再通过obj.age.number读取number属性时,就会建立响应联系,当修改obj.age.number的值时,就能触发依赖重新执行了。
浅响应
并不是所用情况我们都希望深响应,这就催生了shallowReactive,即浅响应。所谓浅响应,指的是只有对象的第一层属性是响应的,实现该功能并不难,只需要多加一次判断即可:
// 封装createReact函数时,接收一个参数isShallow,代表是否为浅响应,默认为false,即非浅响应
function createReact(target, isShallow = false) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
if(isShallow) {
return res
}
if(typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
})
}
实现reactive和shallowReactive
接下来我们就可以轻松实现reactive和shallowReactive函数了:
function reactive(target) {
return createReact(target)
}
function shallowReactive(target) {
return createReact(target, true)
}
reactive实例
下面是reactive的完整实例和代码:
<div id="app"></div>
<div id="app1"></div>
const obj = reactive({
name: "张三",
age: {
number: 1
}
})
effect(() => {
document.querySelector('#app').innerText = obj.name
})
effect(() => {
document.querySelector('#app1').innerText = obj.age.number
})
setTimeout(() => {
obj.name = '李四'
obj.age.number = 2
}, 2000);
function reactive(target) {
return createReact(target)
}
function shallowReactive(target) {
return createReact(target, true)
}
function createReact(target, isShallow = false) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
if(isShallow) {
return res
}
if(typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
})
}
const targetMap = new WeakMap()
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
let activeEffect
class ReactiveEffect {
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
// 收集依赖
function track(target, key) {
if(!activeEffect) return
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()))
}
trackEffects(dep)
}
function trackEffects(dep) {
dep.add(activeEffect)
}
// 触发依赖
function trigger(target, key) {
const depsMap = targetMap.get(target)
if(!depsMap) return
const dep = depsMap.get(key)
if(!dep) return
triggerEffects(dep)
}
function triggerEffects(dep) {
const effects = Array.isArray(dep) ? dep : [...dep]
for(const effect of effects) {
effect.run()
}
}
ref
reactive返回的是一个代理对象,因此reactive并不能处理基本数据类型,要想解决基本数据类型的响应式,我们可以考虑将传入ref的参数封装在一个实例里,在该实例的内部实现getter和setter方法实现依赖的收集和触发。
function ref(value) {
return createRef(value)
}
function createRef(value) {
return new RefImpl(value)
}
class RefImpl {
constructor(value) {
// 原始值
this._rawValue = value
// 如果value是复杂数据类型,会通过reactive生成一个proxy实例
// 即this._value = reactive(value)
this._value = toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
if(hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = toReactive(newVal)
triggerRefValue(this)
}
}
}
// 收集依赖
function trackRefValue(ref) {
if(activeEffect) {
trackEffects(ref.dep || (ref.dep = new Set()))
}
}
function trackEffects(dep) {
dep.add(activeEffect)
}
// 触发依赖
function triggerRefValue(ref) {
if(ref.dep) {
triggerEffects(ref.dep)
}
}
function triggerEffects(dep) {
const effects = Array.isArray(dep) ? dep : [...dep]
for(const effect of effects) {
effect.run()
}
}
// 判断value是否为一个对象,是则返回 reactive返回的代理对象,不是则返回原值
function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
// 判断val是否为一个对象,是则返回true
function isObject(val) {
return val !== null && typeof val === 'object'
}
// 对比两个数据是否发生变化
function hasChanged(value, oldValue) {
return !Object.is(value, oldValue)
}
以下是RefImpl实例的结构,ref会将依赖存储到该实例内部:
ref实例
ref接收的是对象:
<div id="app"></div>
const obj = ref({
name: "张三"
})
effect(() => {
document.querySelector("#app").innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = "李四"
}, 1000)
上面使用effect注册这个副作用函数的时候其实触发了2次getter,一次是refImpl实例value属性的getter,该getter读取到的是一个代理对象,还有一次就是这个代理对象name属性的getter。这2次getter行为都会对依赖进行收集,value属性的getter会将依赖收集到refImpl实例内部的dep(Set数据结构)中,代理对象name属性的getter会将依赖收集到WeakMap中。
1秒后通过obj.value.name修改属性值时,触发的是代理对象name属性的setter,value的setter并不会触发,所以最终通过触发WeakMap中的依赖改变视图。
ref接收的是普通数据类型:
<div id="app"></div>
const obj = ref('张三')
effect(() => {
document.querySelector("#app").innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 1000)
如果ref接收的是一个普通数据类型,上面代码中的effect注册副作用函数时触发了refImpl实例中value属性的getter,该getter会将依赖收集到refImpl实例的dep(Set数据结构)中,通过obj.value修改属性值时会触发value的setter,最终触发存储在refImpl实例中的依赖修改视图。