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数据结构,去除添加重复的effectdep.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数据结构,去除添加重复的effectdep.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 = newValtrigger(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 = newValtrigger(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.结合单元测试用例,看边界测试用例的执行条件

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值