likes
comments
collection
share

Vue3 响应式原理

作者站长头像
站长
· 阅读数 18

Vue3 的响应式采用ES6 的新增API  Proxy 语法实现了对目标对象的数据劫持,在getter中使用track函数收集 effect(副作用),在setter中使用trigger函数执行收集的依赖。 接下来从一个实际用例开始,手把手实现一个乞丐版本的reactivity库,实现了 reactive, track, trigger, effect, ref, computed 等API

非响应式的JS代码

let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity
console.log('total: ' + total) // 10
product.price = 10
console.log('total: ' + total) // 10, 由于product对象不具备响应式的能力,product.price变了,但是total还是10,而不是20

改造第一步

定义一个effect函数专门处理total计算的副作用,执行track函数把effect进行收集, trigger函数执行目标对象的effect,  更新total的值

let product = { price: 5, quantity: 2 }
let total = 0
// 专门用于计算total的值
let effect = () => {
    total = product.price * product.quantity
}
 
// 使用WeakMap作为存储容器,WeakMap的key必须是对象
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使用Set数据结构,去除添加重复的effect
    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()
        })
    }
}
// 追踪 product.price的变化
track(product, 'price')
// 初始化执行一次
effect()
console.log('total: ' + total) // 10
product.price = 10
// 触发product.price 的保存的effect, 重新执行effect,更新total函数
trigger(product, 'price')
console.log('total: ' + total) // 20

改造第二步

实现reactive函数,使用proxy 代理target,在get中自动执行track的对应的key的effect,  在set中trigger执行对应key的effect

function reactive(target) {
    let handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver)
            // 执行取值操作追踪
            track(target, key)
            return result
        },
        set(target, key, receiver) {
            let oldVal = target[key]
            let result = Reflect.set(target, key, receiver)
            if (oldVal !== result) {
                // 执行赋值操作触发
                trigger(target, key)
            }
            return result
        }
    }
    let proxy = new Proxy(target, handler)
    return proxy;
}
 
 
// 使用WeakMap作为存储容器,WeakMap的key必须是对象
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使用Set数据结构,去除添加重复的effect
    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 = reactive({ price: 5, quantity: 2 })
let total = 0
// 专门用于计算total的值
let effect = () => {
    total = product.price * product.quantity
}
// 初始化执行一次
effect()
console.log('total: ' + total) // 10
product.price = 10
console.log('total: ' + total) // 20

改造第三步

实现类似源码中的effect函数,effect变成为包装函数,使用activeEffect的变量保存当前执行的effect(源码中使用的effectStack的栈存储,乞丐版本暂不考虑effect嵌套的复杂的情况)

let activeEffect = null
function effect(eff){
    activeEffect = eff;
    activeEffect()
    activeEffect = null
}
function reactive(target) {
    let handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver)
            // 执行取值操作追踪
            track(target, key)
            return result
        },
        set(target, key, receiver) {
            let oldVal = target[key]
            let result = Reflect.set(target, key, receiver)
            if (oldVal !== result) {
                // 执行赋值操作触发
                trigger(target, key)
            }
            return result
        }
    }
    let proxy = new Proxy(target, handler)
    return proxy;
}
 
 
// 使用WeakMap作为存储容器,WeakMap的key必须是对象
const targetMap = new WeakMap()
 
function track(target, key) {
   if (activeEffect != null) {
        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使用Set数据结构,去除添加重复的effect
         dep.add(activeEffect)
    }
     
}
 
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 = reactive({ price: 5, quantity: 2 })
let total = 0
let salePrice = 0;
// 专门用于计算total的值
effect(() => {
    total = product.price * product.quantity
})
// 计算销售价格
effect(() => {
    salePrice = product.price * 0.9
})
console.log('total: ' + total + ', salePrice: ' + salePrice ) // total: 10, salePrice: 4.5
product.price = 10
console.log('total: ' + total + ', salePrice: ' + salePrice ) // total: 20, salePrice: 9

改造第四步

实现ref的函数,ref的实现利用对象get,set的存储器特性,在get中track 为 value的key, 在set中trigger为 value的key;

let activeEffect = null
function effect(eff){
    activeEffect = eff;
    activeEffect()
    activeEffect = null
}
function reactive(target) {
    let handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver)
            // 执行取值操作追踪
            track(target, key)
            return result
        },
        set(target, key, receiver) {
            let oldVal = target[key]
            let result = Reflect.set(target, key, receiver)
            if (oldVal !== result) {
                // 执行赋值操作触发
                trigger(target, key)
            }
            return result
        }
    }
    let proxy = new Proxy(target, handler)
    return proxy;
}
 
 
// 使用WeakMap作为存储容器,WeakMap的key必须是对象
const targetMap = new WeakMap()
 
function track(target, key) {
   if (activeEffect != null) {
        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使用Set数据结构,去除添加重复的effect
         dep.add(activeEffect)
    }
     
}
 
function trigger(target, key) {
    let depsMap = targetMap.get(target)
    if (!depsMap) return;
    let dep = depsMap.get(key)
    if (dep) {
        dep.forEach(effect => {
            effect()
        })
    }
}
 
 
function ref(raw) {
    const r = {
        get value() {
            track(r, 'value')
            return raw
        },
        set value(newVal) {
            raw = newVal
            trigger(r, 'value')
            return raw
        }
    }
    return r
}
 
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let salePrice = ref(0);
// 计算销售价格
effect(() => {
    salePrice.value = product.price * 0.9
})
// 计算总价 = 打折的价格 * 数量
effect(() => {
    total = salePrice.value * product.quantity
})
 
console.log('total: ' + total + ', salePrice: ' + salePrice.value ) // total: 9, salePrice: 4.5
product.price = 10
console.log('total: ' + total + ', salePrice: ' + salePrice.value ) // total: 18, salePrice: 9

改造第五步

计算总价和销售价格使用computed函数更加合适,实现computed函数(源码不仅处理了getter也处理了setter, 乞丐版本只考虑getter了)

let activeEffect = null
function effect(eff){
    activeEffect = eff;
    activeEffect()
    activeEffect = null
}
function reactive(target) {
    let handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver)
            // 执行取值操作追踪
            track(target, key)
            return result
        },
        set(target, key, receiver) {
            let oldVal = target[key]
            let result = Reflect.set(target, key, receiver)
            if (oldVal !== result) {
                // 执行赋值操作触发
                trigger(target, key)
            }
            return result
        }
    }
    let proxy = new Proxy(target, handler)
    return proxy;
}
 
 
// 使用WeakMap作为存储容器,WeakMap的key必须是对象
const targetMap = new WeakMap()
 
function track(target, key) {
   if (activeEffect != null) {
        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使用Set数据结构,去除添加重复的effect
         dep.add(activeEffect)
    }
     
}
 
function trigger(target, key) {
    let depsMap = targetMap.get(target)
    if (!depsMap) return;
    let dep = depsMap.get(key)
    if (dep) {
        dep.forEach(effect => {
            effect()
        })
    }
}
 
 
function ref(raw) {
    const r = {
        get value() {
            track(r, 'value')
            return raw
        },
        set value(newVal) {
            raw = newVal
            trigger(r, 'value')
            return raw
        }
    }
    return r
}
 
function computed(getter) {
    // 创建个响应式的ref保存结果
    const result = ref()
     
    // 在effect中执行getter函数并把结果赋值给result, 这样getter中的数据发生了变化会重新执行effect, 实现计算属性
    effect(() =>  {
        result.value = getter()
    })
    // 返回执行的结果
    return result;
}
let product = reactive({ price: 5, quantity: 2 })
// 计算销售价格
let salePrice = computed(() => {
    return product.price * 0.9
})
// 计算总价 = 打折的价格 * 数量
let total = computed(() => {
    return salePrice.value * product.quantity
})
console.log('total: ' + total.value + ', salePrice: ' + salePrice.value ) // total: 9, salePrice: 4.5
product.price = 10
console.log('total: ' + total.value + ', salePrice: ' + salePrice.value ) // total: 18, salePrice: 9
 
 
// 由于使用的proxy的原因,直接把新增的属性变成响应式,不需要Vue2那样使用特定的API处理, Vue.$set(target, key, value)
// 手写的乞丐版使用了proxy当然也支持,请看下面的栗子
product.name = 'Iphone'
effect(() => {
    console.log('product name is ' + product.name)
})
product.name = 'MacbookPro'
// 新增的name属性变化了,第一次打印 Iphone, 赋值后重新执行effect 打印 MacbookPro

学习Vue3的响应式源码

手写代码到此为止,相信大家都可以写出一个乞丐版的reactivity库。

如果还剩时间的话,与大家一起逐行阅读Vue3的reactvity的源码,帮助大家继续深入理解reactivity中的优秀实践和一些边界情况的处理

源码的学习方式

  1.  阅读大神的博客和相关文章

  2.  自己看源码, 结合常用的使用场景

    1. 找到源码入口,
    2. 阅读主干逻辑,跳过边界的条件判断
    3. 不理解的地方,使用日志调试或者断点调试弄清楚代码的执行逻辑
    4. 结合单元测试用例,看边界测试用例的执行条件