vue3.0响应式系统的实现原理(Vue3.0采用新特性Proxy来实现数据状态的响应)
vue3.0响应式系统的实现原理(Vue3.0采用新特性Proxy来实现数据状态的响应)Object.keys(value).forEach((key) => { defineReactive(value key value[key]); }); 接下来当我们去使用 state 对象的时候,就能劫持到内部的行为。最后就是构造一个 Proxy 对象完成数据的响应式。相比 Object.defineproperty 一开始就要递归遍历整个对象的做法来说,使用 Proxy 性能会好得多。比如原来 forEach 遍历:首先判断传入的参数类型是否可以用于观察,目前支持的类型为 Object|Array|Map|Set|WeakMap|WeakSet。接下来判断参数的构造函数,根据类型获得不同的 handlers。这里我们就统一使用 baseHandlers,因为这个已经覆盖 99% 的情况了。只有 Set Map WeakMap WeakSet 才会使用到 col
from-> yck:https://juejin.im/post/5d996e3e6fb9a04e3043cc5b
Vue3 使用了 Proxy 替换了原来的 Object.defineProperty 来实现数据响应。
reactive()使用很简单,直接Vue引入reactive方法,接收一个对象参数,就实现了数据的响应式:
const state = reactive({ num: 0 }) effect(() => { console.log(state.num) }) state.num = 100
reactive 内部的核心代码简化如下:
首先判断传入的参数类型是否可以用于观察,目前支持的类型为 Object|Array|Map|Set|WeakMap|WeakSet。
接下来判断参数的构造函数,根据类型获得不同的 handlers。这里我们就统一使用 baseHandlers,因为这个已经覆盖 99% 的情况了。只有 Set Map WeakMap WeakSet 才会使用到 collectionHandlers。
对于 baseHandlers 来说,最主要的是劫持了 get 和 set 行为,这两个行为同时也能原生劫持数组下标修改值及对象新增属性的行为,这一点非常重要,因为Object.defineProperty就不行。
最后就是构造一个 Proxy 对象完成数据的响应式。相比 Object.defineproperty 一开始就要递归遍历整个对象的做法来说,使用 Proxy 性能会好得多。比如原来 forEach 遍历:
Object.keys(value).forEach((key) => { defineReactive(value key value[key]); });
接下来当我们去使用 state 对象的时候,就能劫持到内部的行为。
读取时:state.num 就会触发 get 函数;
修改时:state.num = 100 就会触发 set 函数。
以下是这两个函数的核心(TS语法):
function get(target: any key: string | symbol receiver: any) { // 获得结果 const res = Reflect.get(target key receiver) track(target OperationTypes.GET key) // 判断是否为对象,是的话将对象包装成 proxy return isObject(res) ? reactive(res) : res }
对于 get 函数来说,获取值肯定是最核心的一步骤了。接下来是调用 track,这个和 effect 有关,下文再说。最后是判断值的类型,如果是对象的话就继续包装成 Proxy。
function set( target: any key: string | symbol value: any receiver: any ): boolean { const result = Reflect.set(target key value receiver) if (是否新增 key) { trigger(target OperationTypes.ADD key) } else if (value !== oldValue) { trigger(target OperationTypes.SET key) } return result }
对于 set 函数来说,设置值是第一步骤,然后调用 trigger,这也是 effect 中的内容。
简单来说,如果某个 effect 回调中有使用到 state.num,那么这个回调会被收集起来,并在调用 state.num =100 时触发。
那么怎么收集这些内容呢?这就要说说 targetMap 这个对象了。它用于存储依赖关系,类似以下结构,这个结构会在 effect 文件中被用到
{ target: { key: Dep } }
先来解释下三者到底是什么,这个很重要:
- target 就是被 proxy 的对象
- key 是对象触发 get 行为以后的属性。比如 counter.num 触发了 get 行为,num 就是 key
- dep 是回调函数,也就是 effect 中调用了 counter.num 的话,这个回调就是 dep,需要收集起来下次使用
这里笔者把这些内容脱离源码串起来讲一下流程。
const counter = reactive({ num: 0 }) effect(() => { console.log(counter.num) }) counter.num = 7
首先创建一个 Proxy 对象,targetMap 会把这个对象收集起来当做 key。
接下来调用 effect 回调的时候会把这个回调保存起来,用于下面的依赖收集。在调用的过程中会触发 counter 的 get 函数,内部调用了 track 函数,这个函数会使用到 targetMap。
这里首先通过 target 从 targetMap 中取到一个对象,这个对象也就是 target 所有的依赖关系。那么对于 counter.num 来说,num 就是这个对象的 key(这里如果有点模糊的话可以先看下上面的数据结构),值是一个依赖回调的集合,因为 counter.num 可能会被多个地方依赖到。
回调执行完毕以后会把保存的回调销毁掉。
当我们调用 counter.num = 7 时,触发 set 函数,内部调用 trigger 函数,同样会使用到 targetMap。
同样通过 target 取到一个对象,然后通过 key 也就是 num 去取出依赖集合,最后遍历这个集合执行里面所有的回调函数。
另外对于 computed 来说,内部也是使用到了 effect,无非它的回调不会在调用 effect 后立即执行,只有当触发 get 行为以后才会执行回调并进行依赖收集,举个例子:
const state= reactive({ num: 0 }) const cValue = computed(() => state.num) state.num = 1
对于以上代码来说,computed 的回调永远不会执行,只有当使用到了 cValue.value 时才会执行回调,然后接下来的操作就和上面的没区别了。