Vue3 响应式原理
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中的优秀实践和一些边界情况的处理
源码的学习方式
-
阅读大神的博客和相关文章
-
自己看源码, 结合常用的使用场景
- 找到源码入口,
- 阅读主干逻辑,跳过边界的条件判断
- 不理解的地方,使用日志调试或者断点调试弄清楚代码的执行逻辑
- 结合单元测试用例,看边界测试用例的执行条件
转载自:https://juejin.cn/post/7151944989264576526