首先,Vue3 与 Vue2 采用了不同的实现方式,option api 将所有的属性与方法全部挂在在 this 中,造成了类型推断困难,而 composition api 重新用 TypeScript 以及 ES6 中的 Proxy 和 Reflect 编写改进,
Effect
响应性的本质实际上是发布订阅设计模式,我们需要在变量发生改变时执行一次 effect,也就是变量发生改变时所造成的影响。
let a = 0
let b = 1
let sum
function effect() {
sum = a + b
}
effect()
console.log(sum) // 1
a = 3
effect()
console.log(sum) // 4
复制代码
此时我们手动执行了 effect 函数,但是我们希望 effect 函数是自动执行的,每当我们更改了 effect 中的某个变量 effect 就重新执行,为了实现这个目标,我们引入了 Proxy。
Proxy
我们新建一个 reactive 函数,这个函数接受一个 target 变量,返回一个被 Proxy 封装后的对象,每次更改对象中的值时,都会调用一次 effect 函数重新计算结果
function reactive(target) {
let handler = {
set(target, key, value) {
target[key] = value
effect()
},
}
return new Proxy(target, handler)
}
function effect() {
sum = obj.a + obj.b
}
let obj = reactive({
a: 0,
b: 1,
})
let sum
effect()
console.log(sum) // 1
obj.a = 3
console.log(sum) // 4
复制代码
到此为止本质上来说我们已经实现了最基本的响应式,但是我们当前的响应式只能执行同一个函数,当我们需要使用别的函数时只能修改 reactive 内部 set 函数执行的内容。为了使我们的响应式可以自定义需要执行的函数,且可以针对不同的变量有不同的执行函数,我们开始维护一些全局变量。
deps和depsMap
我们会在 reactive handler 的 get 中保存 effect,在 set 中执行所有的 effect。
const depsMap = new Map()
let activeEffect = null
function reactive(target) {
let handler = {
get(target, key, receiver) {
if (activeEffect) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver)
let dep = depsMap.get(key)
dep.forEach((effect) => effect())
return result
},
}
return new Proxy(target, handler)
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
let obj = reactive({
a: 5,
b: 1,
})
let sum, product
effect(() => {
sum = obj.a + obj.b
})
effect(() => {
product = obj.a * 3
})
console.log('sum', sum) // sum 6
console.log('product', product) // product 15
obj.a = 3
console.log('sum', sum) // sum 4
console.log('product', product) // product 9
复制代码
在上面的代码中,我们做了如下修改
- 新增了
depsMap
集合,depsMap
中保存了所有属性的对应的要执行的effect Set集合; - 新增了一个
activeEffect
全局变量,这个变量中保存着现在运行的effect; - 修改
effect
函数,下面我们给出effect函数的执行过程
当我们修改reactive包裹的对象的值时,会调用Proxy.handler.set
方法,我们先将target[key]修改,接着运行所有的effect(这里有优化空间,我们将在下面讲述)
我们在这里使用了ES6中的
Reflect.get()
和Reflect.set()
方法,目的是在存在继承时可以有正确的this
指向,详情参考 在es6 Proxy中,推荐使用Reflect.get而不是target[key]的原因
到此为止,我们已经完成了大部分的响应式的内容,但是我们可以注意到我们现在仅仅有一个reactive对象,当我们拥有多个reactive对象,且这些reactive对象中拥有相同的属性时,会出现一些错误,为了解决这个问题,我们在这里加入targetMap
(weakMap),用来保存不同对象的depsMap
。
targetMap,track和trigger
我们首先封装track
和trigger
函数
function track(target, key) {
if (activeEffect) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
}
function trigger(target, key) {
let dep = depsMap.get(key)
dep.forEach((effect) => effect())
}
复制代码
接下来我们将depsMap
移入targetMap
中
const targetMap = new WeakMap()
let activeEffect = null
function reactive(target) {
let handler = {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
}
return new Proxy(target, handler)
}
function track(target, key) {
if (activeEffect) {
// 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
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())
}
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
复制代码
到这里基本就要大功告成啦!我们将depsMap移入了targetMap中,如果不存在就新建并且将其放入targetMap。
最后我们再进行一点点优化
const targetMap = new WeakMap()
let activeEffect = null
function reactive(target) {
let handler = {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 保存oldValue
let oldValue = target[value]
let result = Reflect.set(target, key, value, receiver)
// 当oldValue和新的值不同时,执行所有的effect
if (oldValue !== value) trigger(target, key)
return result
},
}
return new Proxy(target, handler)
}
function track(target, key) {
if (activeEffect) {
// 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
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())
}
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
复制代码
在Proxy.handler.set中,我们加入了对过去值和当前值的判断,当且仅当过去的值和当前修改的值不同时才执行dep中的effect函数
最后的最后,我们再来实现一个ref
ref
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newValue) {
let oldValue = this.value
raw = newValue
if (oldValue !== newValue) trigger(r, 'value')
},
}
return r
}
复制代码
有了前面的铺垫,ref的实现就变得简单了起来,由于在Vue中ref包裹的变量都需要通过.value访问,我们采用了对象的属性访问器getter
和setter
来实现对于value的访问。属性访问器的功能类似于前面reactive中使用的Proxy,每当获取值的时候执行track
保存当前的activeEffect
,设置值的时候判断是否和之前的值相等,如果不相等的话执行trigger
好了,以上所有就是Vue响应式的基本原理了,本文章主要来源于学习Vue Master的一些笔记和总结,希望对大家有所帮助。
本文使用 文章同步助手 同步