Vue 3 核心原理 -- reactivity 自己实现

标签(空格分隔): vue 前端 [toc] 前言 为了更好地理解 vue3,阅读其源码是一个重要的途径,但是单纯阅读源码可能理解不了作者为什么这样写,因此自己根据 API 来实现一遍然后再与源码作对比,可以更深刻理解 vue3 的实现原理。 该部分源码复写,可看Vue 3 核心原理 -- reactivity 源码复写 自己实现一遍 自己实现前,需要了解一下 vue3 的 API 以及大概的实现原理,可参考以下文章: Vue3 Composition API RFC Vue 3 源码导读 "Vue3 中的数据侦测" es6 proxy vue 3 一个简单的例子如下 vue 3 使用以上 API 设计的一大原因就是让业务代码可以更加高内聚低耦合,还有其他原因可以参考 API RFC。 根据以上 API ,开始动手写 ref ref 与 reactive 两个函数都是用于监听数据修改,实现数据绑定的。 两者的区别在于 ref 是用于监听原始值的。 因为 js 原始值不能引用内存地址,就算修改了也无从知晓,因而可以将其包装成一个对象,这样就可以获取到这个变量的引用,监听修改。 ref 只有一个 value 属性。 已知使用 ref 监听变量修改,使用 watch 订阅通知。 // 以下按序号阅读,可以复制全部运行。 // 4.2 新建一个保存订阅的 WeakMap,使用 WeakMap 防止内存泄漏 const subscription = new WeakMap() // 7.2 当前需要加入订阅列表的回调 let currentSub // ---- 例子 ---- const count = ref(0) const double = computed(() => {console.log('computed'); return count.value * 2}) // log computed #这里没做 lazy watch(() => console.log(count.value, double.value)) // log 0 0 // log computed count.value++ // log 1 2 connsole.log(double.value) // log 2 # 没有 log computed 证明缓存了 // ------------- // 1. 先写 ref 函数,已知使用 proxy function ref(value) { const innerObj = { value } return new Proxy(innerObj, { get(obj, key, receiver) { if (key !== 'value') { return } // 9.1 收集依赖,即 将当前订阅放入到每个触发了 getter 的变量的订阅列表中 track(obj, key) return Reflect.get(obj, key, receiver) }, // 2. 暂不知道如何收集依赖,先写 set,修改变量就要通知订阅 set(obj, key, v, receiver) { if (key !== 'value') { return false } const res = Reflect.set(obj, key, v, receiver) // 3.1 通知该变量订阅的修改 trigger(obj, key) return res } }) } // 9.2 收集依赖 function track(obj, key) { if (!currentSub) { return } let subList = subscription.get(obj) if (!subList) { subList = new Set() subscription.set(obj, subList) } subList.add(currentSub) } // 3.2 通知 function trigger(obj, key) { // 4.1 所有订阅应该在一个列表上才能通知到 // 5. 获取当前监听变量的订阅列表 const subList = subscription.get(obj) // Set if (!subList) { return } // subList.forEach((cb) => cb()) // 13. 先执行 computed 再执行 watch Array.from(subList) .sort((a, b) => Number(!!b.computed) - Number(!!a.computed)) .forEach(cb => cb()) } // 6. 既然发现有一个订阅列表了,那么 watch 的时候就是将订阅放入对应的列表 function watch(cb, opt = {}) { // 12. 标记 computed cb.computed = opt.computed // 7.1 怎么放? 可以使用一个全局变量来标记当前的订阅 currentSub = cb // 8. 执行一下,这样可以触发变量的 getter cb() // 10. 收集完成,清理 currentSub = null } // 11. 最后实现 computed,其实就是 watch 的 lazy 版,触发订阅时注意要先 computed 再到 watch function computed(getter, setter) { // 数值要缓存起来,不要每次都算 let value // 这里将订阅放到 computed 所依赖的变量的订阅列表,就是 count watch(() => { value = getter() }, { computed: true }) return new Proxy({}, { get(obj, key, receiver) { if (key !== 'value') { return } return value }, set(obj, key, v, receiver) { if (key !== 'value' || !setter) { return false } return setter(obj, key, v, receiver) } }) } reactive 有了 ref 的实现思路,实现 reactive 就很简单了。 ref 其实可以算是 reactive 的特化版 -- 只包装 { value } 对象。 reactive 需要实现对象的遍历监听以及属性增删的监听。其他跟 ref 类似的代码就不写注释了,只写 reactive 特有的 const subMap = new WeakMap() let currentCb // 保存已经 reactive 的对象 const alreadyReactive = new WeakMap() // --- 例子 --- const state = reactive({ count: 0, obj: {a: 0}, arr: [1,2,3] }) watch(() => { console.log('count:', state.count) }) watch(() => { console.log('obj.a:', state.obj.a) }) // 这里有问题了,watch 的时候只收集到子对象的,没收集到子对象的属性,那么就监听不到了其属性修改 watch(() => { console.log('obj:', state.obj) }) watch(() => { console.log('arr:', state.arr) }) state.count++ // log count: 1 state.obj.a++ // log obj.a: 1 // 这里就监听不到了,要查看源码才知道什么做 state.arr.push(4) state.arr.pop() state.arr[0] = 11 // --------- function reactive(target) { const cache = alreadyReactive.get(target) if (cache) { return cache } const proxy = new Proxy(target, { get(obj, key, receiver) { track(obj, key) const res = Reflect.get(obj, key, receiver) // 如果属性是对象,就返回一个 reactive 包装的对象,递归遍历 // 由于频发触发 reacitve 函数有性能问题,因此可以缓存起来 // 使用 alreadyReactive 保存已包装过的对象 return typeof res === 'object' ? reactive(res) : res }, set(obj, key, value, receiver) { const res = Reflect.set(obj, key, value, receiver) trigger(obj, key) return res } }) alreadyReactive.set(target, cache) return proxy } // 由于对象有多个属性,每个属性都有对应的订阅列表 // 因此容器 subMap 的数据结构为 WeakMap function trigger(obj, key) { const target = subMap.get(obj) if (!target) { return } const sub = target.get(key) if (!sub) { return } sub.forEach(cb => cb()) } function track(obj, key) { if (!currentCb) { return } let target = subMap.get(obj) if (!target) { target = new Map() subMap.set(obj, target) } let sub = target.get(key) if (!sub) { sub = new Set() target.set(key, sub) } sub.add(currentCb) } function watch(cb) { currentCb = cb cb() currentCb = null } 以上就是 ref reactive 的核心原理,带着自己实现时的理解与疑惑, 再去阅读源码,更容易理解作者的思路与实现。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):