Vue3响应式原理的学习:Vue Mastery上的Vue3 Reactivity
B站中文字幕Vue3响应式原理(Vue3 Reactivity)
Github地址vue-3-reactivity
基础响应式
let price = 5
let quantity = 2
let total = price * quantity
console.log(`total is ${total}`)
price = 20
console.log(`total is ${total}`)
普通JS代码执行中,并不会有响应式,在修改price变量的值之后,total的值并没有改变。
所以,现在需要做的就是将修改total值的方法保存起来,等到与total有关的变量price和quantity变化时,重新触发该方法。
let price = 5
let quantity = 2
let total = 0
let dep = new Set() // 存储effect方法
// 修改total值的方法,需要被保存起来
let effect = () => {
total = price * quantity
}
// 保存修改total值得方法
function track(){
dep.add(effect)
}
// 运行保存的方法
function trigger(){
dep.forEach(effect => effect())
}
track() // total: 0
trigger() // total: 10
price = 10 // total: 10
trigger() // total: 20
代码中的effect、track、trigger在Vue3响应式源代码中都有相同名称的方法
通常,我们的对象会有多个属性,每个属性都需要自己的dep或者说是effect的Set集,那么,我们该如何存储,或者说让每个属性都有自己的dep呢?
这里引入一个Map类型集合(depsMap), 其key为对象的属性,value为每个属性对应的dep
const depsMap = new Map()
function track(key){
let dep = depsMap.get(key) // 获取每个key所对应的dep
if(!dep){
depsMap.set(key, (dep = new Set()))
}
dep.add(effect)
}
function trigger(key){
let dep = depsMap.get(key)
if(dep){
dep.forEach(effect => effect())
}
}
let product = { price:5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track('quantity')
effect() // total: 10
product.quantity = 3
trigger('quantity') // total: 15
此时,我们可以对不同的属性有一种追踪依赖的方法,但是如果我们有多个响应式对象呢?
我们需要一个targetMap用来存储每个响应式对象的依赖
const targetMap = new WeakMap()
function track(target, key){
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(effect)
}
function trigger(target, key){
let depsMap = targetMap.get(target)
if(!depsMap) return
let dep = depsMap.get(key)
if(dep){
dep.forEach(effect => effect())
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track(product, 'quantity')
effect() // total: 10
product.quantity = 3 // total: 10
trigger(product, 'quantity') // total: 15
targetMap存储每个与响应式对象属性关联的依赖,depsMap存储了每个属性的依赖,dep是一个effects集(Set)的依赖,如下图所示:
Proxy and Reflect
现在已经实现了基础的响应式了,但是需要实现自动触发track和trigger。
Reflect
通常我们有三种方法读取一个对象的属性:
product.quantityproduct['quantity']Reflect.get(product, 'quantity')
Proxy
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver){
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value ,receiver)
}
})
receiver确保当我们的对象有继承自其它对象的值或函数时,this能正确的指向使用的对象
reactive
function reactive(target){
const handler = {
get(target, key, receiver){
let result = Reflect.get(target, key, receiver)
track(target, key)
return result
},
set(target, key, value, receiver){
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if(oldValue !== value){
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect() // total: 10
product.quantity = 3 // total: 15
activeEffect
此时的实现,track函数中的依赖effect函数是外部定义的,当依赖发生变化时,track函数收集依赖时需要手动修改,我们只应在effect里调用追踪函数
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)
}
}
let product = reactive({ price: 5, quantity: 2 })
let salePrice = 0
let total = 0
effect(() => {
total = product.price * product.quantity
})
effect(() => {
salePrice = product.price * 0.9
})
// total: 10 salePrice: 4.5
product.quantity = 3
// total: 15 salePrice: 4.5
product.price = 10
// total: 20 salePrice: 9
此时salePrice并不是一个响应式的
ref
- 使用
reactive函数
function ref(intialValue){
return reactive({ value: intialValue })
}
- 对象的计算属性(getter、setter)
function ref(raw){
const r = {
get value(){
track(r, 'value')
return raw
},
set value(newVal){
// 注意此处需要加上判断,如果先使用会导致死循环
if(raw!==newVal){
raw = newVal
trigger(r, 'value')
}
}
}
return r
}
Vue3采用的是第二种方式
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
salePrice.value = product.price * 0.9
})
effect(() => {
total = salePrice.value * product.quantity
})
// salePrice: 4.5 total: 9
product.quantity = 3
// salePrice: 4.5 total: 13.5
product.price = 10
// salePrice: 9 total: 27
computed
function computed(getter){
let result = ref()
effect(() => result.value = getter())
return result
}
let salePrice = computed(() => {
return product.price * 0.9
})
let total = computed(() => {
return salePrice.value * product.quantity
})
// salePrice.value: 4.5 total.value: 9
product.quantity = 3
// salePrice.value: 4.5 total.value: 13.5
product.price = 10
// salePrice.value: 9 total.value: 27
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks' // log: Product name is now Socks
Vue3中使用Proxy,新增属性会自动变成响应式
Reactivity源码
在packages/reactivity/src
- effect.ts: 用来定义
effecttracktrigger - baseHandlers.ts: 定义Proxy处理器(get和set)
- reactive.ts: 定义reactive方法并创建ES6 Proxy
- ref.ts: 定义reactive的ref使用的对象访问器,调用
track和trigger - computed.ts: 定义计算属性方法(使用
effect并返回一个ref)
Q&A(A by Evan You)
-
Q: 在Vue 2上,我们会调用
depend去保存函数,并用notify去运行函数,在Vue 3中,我们调用track和trigger,为什么?在Vue 3中已经没有
Dep类了,depend和notify中的逻辑被抽离到两个独立函数track和trigger。当调用track和trigger时更像是在追踪什么而不是什么东西正在被依赖 -
在Vue 2中有一个
Dep类,在Vue 3中只有一个Set,为什么会改变?// Vue 2 class Dep{ constructor(){ this.subscribers = [] } depend(){ if(target && !this.subscribers.includes(target)){ this.subscribers.push(target) } } notify(){ this.subscribers.forEach(sub => sub()) } } // Vue 3 let dep = new Set()在Vue 2中有一个
Dep类更容易思考依赖关系,作为一个对象有着某种行为。Vue 3中,实现过程的改变,抽离出depend和notify,在用一个class类声明是没有意义的。 -
如何想到
targetMap->depsMap->dep存储effects?在Vue 2中,使用ES5的getter和setter转换,当遍历对象上的key时,用forEach时会有一个闭包为其属性存储关联的
Dep,所以是不用这么做的,但是在Vue 3中使用Proxy,proxy中的handler会直接接收target和key,没法得到闭包为每个属性存储关联依赖项 -
定义
Ref时,可以通过返回一个reactive去定义,为什么不这么做?两者的不同?Ref根据定义只能暴露一个属性,就是值的本身,如果使用reactive会给它附加一些新的属性。Ref只能包装为一个内部值服务,不应该被当作一个一般的响应式对象。isRef: 返回的ref对象实际上有一些特殊的东西,让我们知道这是一个ref而不是一个响应式对象。性能问题:响应式对象会做更多的检查,而只使用一个字面量去创建ref会更节省性能 -
在Vue3中使用Reflect和Proxy可以在之后添加属性,还有什么其它好处?
当使用Proxy时,响应式转化会变成懒加载,在Vue2中必须尽快完成转换,因为把对象传递给Vue2的响应式时,必须遍历所有的key并当场转化,所以之后,当被访问时,它们已经被转换了。但是在Vue3中,当调用
ractive时,对于一个对象,我们所做的就是返回一个代理对象,仅需要在访问它时才进行转换。(vue2对于对象需要立即遍历以及嵌套对象需要递归遍历处理成响应式,而Vue3中的Proxy仅在需要的时候才处理成响应式)
以上全部内容,如有疑问,欢迎指正。Good Luck!