vue3中的计算属性(vue3的ref,reactive的使用和原理解析)
目录
1.前言
2.比较
3.ref源码解析
4.reactive源码解析
createReactiveObject
handles的组成
get陷阱
set陷阱
5.总结
1.前言
vue3新增了ref ,reactive两个api用于响应式数据 ,Ref 系列毫无疑问是使用频率最高的 api 之一,响应式意味着数据变动 ,页面局部自动更新 。数据类型有基本数据类型(string,number,boolean,undfined,null,symbol) ,引用数据类型(object,array,set,map等) 。如何精准检测跟踪js中所有的数据类型变动 ,并且能够达到vnode的对比后真实dom的渲染?vue中是如何做到的呢?简单实例如下:
import { reactive, ref } from "vue";
import type { Ref } from "vue";
// 定义响应式数据
const count: Ref<number> = ref(0);
function countClick() {
count.value++; // 更新数据
}
// 定义引用类型数据标注
interface TypeForm {
name: string;
num: number;
list?: Array<[]>;
}
const formInline: TypeForm = reactive({
name: "",
num: 0,
});
formInline.name = KinHKin
formInline.num = 100
formInline.list = [1,2,3,4]
效果图:
在线地址:
KinHKinhttps://rondsjinhuajin.github.io/DemoVue/#/但是 ,这只是简单的使用 ,配合了ts的类型标注 ,但是背后的原理是什么呢?🤔
2.比较
先来做个ref ,reactive的比较:
比较维度(kin注) ref reactive 是否响应式对象 JavaScript Proxy 是 是 创建的数据类型 任何值类型的响应式 响应式对象或数组 是否需要.value 属性 是 否 复杂的类型标注 Ref 这个类型 interface自定义 隐式地从它的参数中推导类型 是 是 dom的更新 异步 异步 是否深层次响应 默认深层次 默认深层次不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同 。
ref 被传递给函数或是从一般对象上被解构时 ,不会丢失响应性:
const obj = { foo: ref(1), bar: ref(2) } // 该函数接收一个 ref // 需要通过 .value 取值 // 但它会保持响应性 callSomeFunction(obj.foo) // 仍然是响应式的 const { foo, bar } = obj简言之 ,ref() 让我们能创造一种对任意值的 “引用 ”,并能够在不丢失响应性的前提下传递这些引用 。这个功能很重要 ,因为它经常用于将逻辑提取到 组合函数 中 。
当 ref 在模板中作为顶层属性被访问时 ,它们会被自动“解包 ”,所以不需要使用 .value 。下面是之前的计数器例子 ,用 ref() 代替:
<script setup> import { ref } from vue const count = ref(0) function increment() { count.value++ } </script> <template> <button @click="increment"> {{ count }} <!-- 无需 .value --> </button> </template>请注意 ,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包” 。
3.ref源码解析
对于vue3.2.2x版本的源码位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
执行顺序是ref ->createRef ->new RefImpl 生成实例对象 ,提供get ,set方法
源码中我们可以看到:入口有两个函数默认深层次响应ref ,浅层次使用shallowRef ,参数一个false ,一个是true 。
function ref(value) { return createRef(value, false); } function shallowRef(value) { return createRef(value, true); }接下来就是走createRef这个方法:
function createRef(rawValue, shallow) { if (isRef(rawValue)) { return rawValue; } return new RefImpl(rawValue, shallow); }这个createRef方法接受两个参数 ,一个是传入的基本类型的默认数值 ,一个是否是深层次响应的boolean值 。
function isRef(r) { return !!(r && r.__v_isRef === true); }如果rawValue本就是ref类型的会立即返回rawValue,否则返回一个RefImpl实例。
RefImpl类:
class RefImpl { constructor(value, __v_isShallow) { this.__v_isShallow = __v_isShallow; this.dep = undefined; this.__v_isRef = true; this._rawValue = __v_isShallow ? value : toRaw(value); this._value = __v_isShallow ? value : toReactive(value); } get value() { trackRefValue(this); return this._value; } set value(newVal) { const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal); newVal = useDirectValue ? newVal : toRaw(newVal); if (shared.hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = useDirectValue ? newVal : toReactive(newVal); triggerRefValue(this, newVal); } } }RefImpl类在构造函数中 ,__v_isShallow表示是否是浅层次响应的属性 , 私有的 _rawValue 变量,存放 ref 的旧值 ,_value是ref接受的最新的值 。公共的只读变量 __v_isRef 是用来标识该对象是一个 ref 响应式对象的标记与在讲述 reactive api 时的 ReactiveFlag 相同 。
在const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;这个函数的内部判断是否传入的是一个对象 ,如果是一个对象就返回reactive返回代理对象,否则直接返回原参数。
当我们通过 ref.value 的形式读取该 ref 的值时 ,就会触发 value 的 getter 方法 ,在 getter 中会先通过 trackRefValue 收集该 ref 对象的 value 的依赖 ,收集完毕后返回该 ref 的值 。
function trackRefValue(ref) { if (shouldTrack && activeEffect) { ref = toRaw(ref); { trackEffects(ref.dep || (ref.dep = createDep()), { target: ref, type: "get" /* TrackOpTypes.GET */, key: value }); } } }当我们对 ref.value 进行修改时 ,又会触发 value 的 setter 方法 ,会将新旧 value 进行比较 ,如果值不同需要更新 ,则先更新新旧 value ,之后通过 triggerRefValue 派发该 ref 对象的 value 属性的更新 ,让依赖该 ref 的副作用函数执行更新 。
function triggerRefValue(ref, newVal) { ref = toRaw(ref); if (ref.dep) { { triggerEffects(ref.dep, { target: ref, type: "set" /* TriggerOpTypes.SET */, key: value, newValue: newVal }); } } }4.reactive源码解析
对于vue3.2.2x版本的源码位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
整体描述vue3的更新机制:
在 Vue3 中,通过 track 的处理器函数来收集依赖 ,通过 trigger 的处理器函数来派发更新 ,每个依赖的使用都会被包裹到一个副作用(effect)函数中,而派发更新后就会执行副作用函数 ,这样依赖处的值就被更新了。
Proxy 对象能够利用 handler 陷阱在 get 、set 时捕获到任何变动 ,也能监听对数组索引的改动以及 数组 length 的改动 。
执行顺序是:reactive -> createReactiveObject ->
function reactive(target) { // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) { return target; } return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap); }第三行 isReadonly 函数 确定对象是否为只读对象,IS_READONLY key 确定对象是否为只读对象 。ReactiveFlags 枚举会在源码中不断的与我们见面 ,所以有必要提前介绍一下 ReactiveFlags:
function isReadonly(value) {
return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]);
}
export const enum ReactiveFlags { SKIP = __v_skip, // 是否跳过响应式 返回原始对象 IS_REACTIVE = __v_isReactive, // 标记一个响应式对象 IS_READONLY = __v_isReadonly, // 标记一个只读对象 RAW = __v_raw // 标记获取原始值 IS_SHALLOW = __v_isShallow // 是否浅层次拷贝 }在 ReactiveFlags 枚举中有 5 个枚举值 ,这五个枚举值的含义都在注释里 。对于 ReactiveFlags 的使用是代理对象对 handler 中的 trap 陷阱非常好的应用 ,对象中并不存在这些 key ,而通过 get 访问这些 key 时 ,返回值都是通过 get 陷阱的函数内处理的 。介绍完 ReactiveFlags 后我们继续往下看 。
createReactiveObject
入参部分:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {}
先看 createReactiveObject 函数的签名 ,该函数接受 5 个参数:
target:目标对象 ,想要生成响应式的原始对象 。 isReadonly:生成的代理对象是否只读 。 baseHandlers:生成代理对象的 handler 参数 。当 target 类型是 Array 或 Object 时使用该 handler 。 collectionHandlers:当 target 类型是 Map 、Set 、WeakMap 、WeakSet 时使用该 handler。 proxyMap:存储生成代理对象后的 Map 对象 。这里需要注意的是 baseHandlers 和 collectionHandlers 的区别 ,这两个参数会根据 target 的类型进行判断 ,最终选择将哪个参数传入 Proxy 的构造函数,当做 handler 参数使用 。
逻辑部分:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { // 如何不是对象 曝出警告 返回其原始值 if (!shared.isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object // 如果目标已经是一个代理 ,直接返回 KinHKin译 // 除非对一个响应式对象执行 readonly if (target["__v_raw" /* ReactiveFlags.RAW */] && !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) { return target; } // target already has corresponding Proxy // 目标已经存在对应的代理对象 KinHKin译 const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // only specific value types can be observed. // 只有白名单里的类型才能被创建响应式对象 KinHKin译 const targetType = getTargetType(target); if (targetType === 0 /* TargetType.INVALID */) { return target; } const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers); proxyMap.set(target, proxy); return proxy; }在该函数的逻辑部分 ,可以看到基础数据类型并不会被转换成代理对象,而是直接返回原始值。
并且会将已经生成的代理对象缓存进传入的 proxyMap ,当这个代理对象已存在时不会重复生成 ,会直接返回已有对象 。
也会通过 TargetType 来判断 target 目标对象的类型,Vue3 仅会对 Array 、Object 、Map 、Set 、WeakMap 、WeakSet 生成代理 ,其他对象会被标记为 INVALID ,并返回原始值 。
当目标对象通过类型校验后 ,会通过 new Proxy() 生成一个代理对象 proxy ,handler 参数的传入也是与 targetType 相关 ,并最终返回已生成的 proxy 对象。
所以回顾 reactive api ,我们可能会得到一个代理对象 ,也可能只是获得传入的 target 目标对象的原始值 。
handles的组成
在 @vue/reactive 库中有 baseHandlers 和 collectionHandlers 两个模块 ,分别生成 Proxy 代理的 handlers 中的 trap 陷阱 。
例如在上面生成 reactive 的 api 中 baseHandlers 的参数传入了一个 mutableHandlers 对象 ,这个对象是这样的:
const mutableHandlers = { get, set, deleteProperty, has, ownKeys };通过变量名我们能知道 mutableHandlers 中存在 5 个 trap 陷阱 。而在 baseHandlers 中,get 和 set 都是通过工厂函数生成的 ,以便于适配除 reactive 外的其他 api ,例如 readonly、shallowReactive 、shallowReadonly 等 。
baseHandlers 是处理 Array 、Object 的数据类型的,这也是我们绝大部分时间使用 Vue3 时使用的类型 ,所以笔者接下来着重的讲一下baseHandlers 中的 get 和 set 陷阱 。
get陷阱
上一段提到 get 是由一个工厂函数生成的 ,先来看一下 get 陷阱的种类 。
const get = /*#__PURE__*/ createGetter(); const shallowGet = /*#__PURE__*/ createGetter(false, true); const readonlyGet = /*#__PURE__*/ createGetter(true); const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);get 陷阱有 4 个类型,分别对应不同的响应式 API ,从名称中就可以知道对应的 API 名称 ,非常一目了然 。而所有的 get 都是由 createGetter 函数生成的 。所以接下来我们着重看一下 createGetter 的逻辑 。
从函数的签名看起 ,入参有2个 ,一个是isReadonly ,另一个是shallow ,让使用 get 陷阱的 api 按需使用。
function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { } }函数内部返回一个get函数 ,使用了闭包的方式 ,将get函数中的参数传到handlers中 。
createGetter 的逻辑:
function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { // 如果key是响应式的对象 就返回不是只读 *KinHKin注释* if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) { return !isReadonly; } // 如果key是只读对象 就返回只读是true *KinHKin注释* else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) { return isReadonly; } // 如果key是浅层次响应对象 就返回浅层次是true *KinHKin注释* else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) { return shallow; } // 如果key是原始值对象并且改变的值和原始标记一致 就返回原始值 *KinHKin注释* else if (key === "__v_raw" /* ReactiveFlags.RAW */ && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) { return target; } // 判断传入的值是不是数组 const targetIsArray = shared.isArray(target); // 如果不是只读 并且是数组 // arrayInstrumentations 是一个对象 ,对象内保存了若干个被特殊处理的数组方法,并以键值对的形式存储 。 *KinHKin注释* if (!isReadonly && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) { // 特殊处理数组返回结果 return Reflect.get(arrayInstrumentations, key, receiver); } // 获取 Reflect 执行的 get 默认结果 const res = Reflect.get(target, key, receiver); // 如果是 key 是 Symbol ,并且 key 是 Symbol 对象中的 Symbol 类型的 key // 或者 key 是不需要追踪的 key: __proto__,__v_isRef,__isVue // 直接返回 get 结果 *KinHKin注释* if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res; } // 不是只读对象 执行 track 收集依赖 *KinHKin注释* if (!isReadonly) { track(target, "get" /* TrackOpTypes.GET */, key); } // 是浅层次响应 直接返回 get 结果 *KinHKin注释* if (shallow) { return res; } if (isRef(res)) { // ref unwrapping - skip unwrap for Array + integer key. // 如果是 ref ,则返回解包后的值 - 当 target 是数组,key 是 int 类型时 ,不需要解包 *KinHKin注释* return targetIsArray && shared.isIntegerKey(key) ? res : res.value; } if (shared.isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. // 将返回的值也转换成代理 ,我们在这里做 isObject 的检查以避免无效值警告。 // 也需要在这里惰性访问只读和星影视对象,以避免循环依赖 。*KinHKin注释* return isReadonly ? readonly(res) : reactive(res); } // 不是 object 类型则直接返回 get 结果 *KinHKin注释* return res; }; }从这段 createGetter 逻辑中 ,之前专门介绍过的 ReactiveFlags 枚举在这就取得了妙用 。其实目标对象中并没有这些 key ,但是在 get 中Vue3 就对这些 key 做了特殊处理 ,当我们在对象上访问这几个特殊的枚举值时 ,就会返回特定意义的结果。而可以关注一下 ReactiveFlags.IS_REACTIVE 这个 key 的判断方式 ,为什么是只读标识的取反呢?因为当一个对象的访问能触发这个 get 陷阱时 ,说明这个对象必然已经是一个 Proxy 对象了 ,所以只要不是只读的 ,那么就可以认为是响应式对象了 。
get 的后续逻辑:
继续判断 target 是否是一个数组 ,如果代理对象不是只读的,并且 target 是一个数组 ,并且访问的 key 在数组需要特殊处理的方法里 ,就会直接调用特殊处理的数组函数执行结果,并返回 。
arrayInstrumentations 是一个对象 ,对象内保存了若干个被特殊处理的数组方法 ,并以键值对的形式存储 。
我们之前说过 Vue2 以原型链的方式劫持了数组,而在这里也有类似地作用 ,下面是需要特殊处理的数组 。
对索引敏感的数组方法 includes、indexOf 、lastIndexOf 会改变自身长度的数组方法 ,需要避免 length 被依赖收集 ,因为这样可能会造成循环引用 push 、pop、shift 、unshift 、splice下面的几个key是不需要被依赖收集或者是返回响应式结果的:
__proto__ _v_isRef __isVue在处理完数组后 ,我们对 target 执行 Reflect.get 方法 ,获得默认行为的 get 返回值 。
之后判断 当前 key 是否是 Symbol ,或者是否是不需要追踪的 key ,如果是的话直接返回 get 的结果 res 。
接着判断当前代理对象是否是只读对象 ,如果不是只读的话 ,则运行笔者上文提及的 tarck 处理器函数收集依赖 。
如果是 shallow 的浅层响应式,则不需要将内部的属性转换成代理 ,直接返回 res 。
如果 res 是一个 Ref 类型的对象 ,就会自动解包返回,这里就能解释官方文档中提及的 ref 在 reactive 中会自动解包的特性了 。而需要注意的是 ,当 target 是一个数组类型 ,并且 key 是 int 类型时,即使用索引访问数组元素时 ,不会被自动解包。
如果 res 是一个对象 ,就会将该对象转成响应式的 Proxy 代理对象返回 ,再结合我们之前分析的缓存已生成的 proxy 对象 ,可以知道这里的逻辑并不会重复生成相同的 res ,也可以理解文档中提及的当我们访问 reactive 对象中的 key 是一个对象时 ,它也会自动的转换成响应式对象 ,而且由于在此处生成 reactive 或者 readonly 对象是一个延迟行为 ,不需要在第一时间就遍历 reactive 传入的对象中的所有 key ,也对性能的提升是一个帮助 。
当 res 都不满足上述条件时,直接返回 res 结果 。例如基础数据类型就会直接返回结果 ,而不做特殊处理。最后 ,get 陷阱的逻辑全部结束了 。
set陷阱
set 也有一个 createSetter 的工厂函数,也是通过柯里化的方式返回一个 set 函数 。
set 的函数比较简短 ,所以这次一次性把写好注释的代码放上来 ,先看代码再讲逻辑。
// 纯函数 默认深层次响应 函数不入参 *KinHKin* const set = /*#__PURE__*/ createSetter(); // 纯函数 浅层次响应 函数入参是true *KinHKin* const shallowSet = /*#__PURE__*/ createSetter(true); function createSetter(shallow = false) { return function set(target, key, value, receiver) { let oldValue = target[key]; // 如果原始值是只读and是ref类型and新的value属性不是ref类型 直接返回 if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false; } if (!shallow) { // 如果新的值不是浅层次响应对象,也不是只读 更新旧值 新值为普通对象 *KinHKin* if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue); value = toRaw(value); } // 当不是 只读 模式时 ,判断旧值是否是 Ref ,如果是则直接更新旧值的 value // 因为 ref 有自己的 setter *KinHKin* if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } } // 判断 target 中是否存在 key *KinHKin* const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); // dont trigger if target is something up in the prototype chain of original // 如果目标是原始对象原型链上的属性 ,则不会触发 trigger 派发更新 *KinHKin* if (target === toRaw(receiver)) { // 使用 trigger 派发更新 ,根据 hadKey 区别调用事件 if (!hadKey) { trigger(target, "add" /* TriggerOpTypes.ADD */, key, value); } else if (shared.hasChanged(value, oldValue)) { trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue); } } return result; }; }在 set 的过程中会首先获取新旧与旧值 ,当目前的代理对象不是浅层比较时 ,会判断旧值是否是一个 Ref ,如果旧值不是数组且是一个 ref类型的对象 ,并且新值不是 ref 对象时 ,会直接修改旧值的 value 。
看到这里可能会有疑问,为什么要更新旧值的 value?如果你使用过 ref 这个 api 就会知道 ,每个 ref 对象的值都是放在 value 里的 ,而 ref 与 reactive 的实现是有区别的,ref 其实是一个 class 实例 ,它的 value 有自己的 set ,所以就不会在这里继续进行 set 了 。
在处理完 ref 类型的值后,会声明一个变量 hadKey ,判断当前要 set 的 key 是否是对象中已有的属性 。
接下来调用 Reflect.set 获取默认行为的 set 返回值 result 。
然后会开始派发更新的过程 ,在派发更新前 ,需要保证 target 和原始的 receiver 相等 ,target 不能是一个原型链上的属性 。
之后开始使用 trigger 处理器函数派发更新 ,如果 hadKey 不存在 ,则是一个新增属性 ,通过 TriggerOpTypes.ADD 枚举来标记 。这里可以看到开篇分析 Proxy 强于 Object.defineProperty 的地方 ,会监测到任何一个新增的 key ,让响应式系统更强大 。
如果 key 是当前 target 上已经存在的属性,则比较一下新旧值 ,如果新旧值不一样 ,则代表属性被更新,通过 TriggerOpTypes.SET 来标记派发更新 。
在更新派发完后 ,返回 set 的结果 result ,至此 set 结束 。
5.总结
开始部分讲解了ref,reactive的使用实例 ,如何进行类型的标注 ,配合ts这么使用 ,接着讲解了两者的区别 ,分别需要注意的点 ,还有ref的顶层自动解包。
后面讲解了ref的底层实现的原理 ,源码的分析 ,ref是一个类 ,它有自己的get ,set方法去更新 。不需要依赖底层的所以使用于所有的数据类型做响应式 。
为了让大家属性 Proxy 对响应式系统的影响,着重介绍了响应式基础 API:reactive。分析了 reactive 的实现 ,以及 reactive api 返回的 proxy 代理对象使用的 handlers 陷阱 。并且对陷阱中我们最常用的 get 和 set 的源码进行分析 ,相信大家在看完本篇文章以后,对 proxy 这个 ES2015 的新特性的使用又有了新的理解 。
本文只是介绍 Vue3 响应式系统的第一篇文章 ,所以 track 收集依赖 ,trigger 派发更新的过程没有详细展开,在后续的文章中计划详细讲解副作用函数 effect ,以及 track 和 trigger 的过程 ,如果希望能详细了解响应式系统的源码 ,麻烦大家点个关注免得迷路。如果想继续追踪后续文章 ,也可以关注我的账号或 follow 我的github ,感谢大家的关注❤️
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!