Vue3响应式原理

266 阅读6分钟

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有关的变量pricequantity变化时,重新触发该方法。

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

代码中的effecttracktrigger在Vue3响应式源代码中都有相同名称的方法

通常,我们的对象会有多个属性,每个属性都需要自己的dep或者说是effectSet集,那么,我们该如何存储,或者说让每个属性都有自己的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)的依赖,如下图所示:

微信图片_20220423221749.png

Proxy and Reflect

现在已经实现了基础的响应式了,但是需要实现自动触发tracktrigger

Reflect

通常我们有三种方法读取一个对象的属性:

  1. product.quantity
  2. product['quantity']
  3. 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

微信图片_20220423224951.png

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

  1. 使用reactive函数
function ref(intialValue){
    return reactive({ value: intialValue })
}
  1. 对象的计算属性(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: 用来定义effect track trigger
  • baseHandlers.ts: 定义Proxy处理器(get和set)
  • reactive.ts: 定义reactive方法并创建ES6 Proxy
  • ref.ts: 定义reactive的ref使用的对象访问器,调用tracktrigger
  • computed.ts: 定义计算属性方法(使用effect并返回一个ref)

Q&A(A by Evan You)

  1. Q: 在Vue 2上,我们会调用depend去保存函数,并用notify去运行函数,在Vue 3中,我们调用tracktrigger,为什么?

    在Vue 3中已经没有Dep类了,dependnotify中的逻辑被抽离到两个独立函数tracktrigger。当调用track和trigger时更像是在追踪什么而不是什么东西正在被依赖

  2. 在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类声明是没有意义的。

  3. 如何想到targetMap -> depsMap -> dep存储effects?

    在Vue 2中,使用ES5的getter和setter转换,当遍历对象上的key时,用forEach时会有一个闭包为其属性存储关联的Dep,所以是不用这么做的,但是在Vue 3中使用Proxy,proxy中的handler会直接接收target和key,没法得到闭包为每个属性存储关联依赖项

  4. 定义Ref时,可以通过返回一个reactive去定义,为什么不这么做?两者的不同?

    Ref根据定义只能暴露一个属性,就是值的本身,如果使用reactive会给它附加一些新的属性。Ref只能包装为一个内部值服务,不应该被当作一个一般的响应式对象。isRef: 返回的ref对象实际上有一些特殊的东西,让我们知道这是一个ref而不是一个响应式对象。性能问题:响应式对象会做更多的检查,而只使用一个字面量去创建ref会更节省性能

  5. 在Vue3中使用Reflect和Proxy可以在之后添加属性,还有什么其它好处?

    当使用Proxy时,响应式转化会变成懒加载,在Vue2中必须尽快完成转换,因为把对象传递给Vue2的响应式时,必须遍历所有的key并当场转化,所以之后,当被访问时,它们已经被转换了。但是在Vue3中,当调用ractive时,对于一个对象,我们所做的就是返回一个代理对象,仅需要在访问它时才进行转换。(vue2对于对象需要立即遍历以及嵌套对象需要递归遍历处理成响应式,而Vue3中的Proxy仅在需要的时候才处理成响应式)

以上全部内容,如有疑问,欢迎指正。Good Luck!

15.webp