您现在的位置是:网站首页> 编程资料编程资料

Vue3响应式对象Reactive和Ref的用法解读_vue.js_

2023-05-24 346人已围观

简介 Vue3响应式对象Reactive和Ref的用法解读_vue.js_

一、内容简介

本篇文章着重结合源码版本V3.2.20介绍Reactive和Ref。前置技能需要了解Proxy对象的工作机制,以下贴出的源码均在关键位置备注了详细注释。

备注:本篇幅只讲到收集依赖和触发依赖更新的时机,并未讲到如何收集依赖和如何触发依赖。响应式原理快捷通道。

二、Reactive

1. 关键源码

/*源码位置:/packages/reactivity/src/reactive.ts*/ /**  * 创建响应式代理对象  * @param target 被代理对象  * @param isReadonly 是否只读  * @param baseHandlers 普通对象的拦截操作  * @param collectionHandlers 集合对象的拦截操作  * @param proxyMap 代理Map  * @returns   */ function createReactiveObject(   target: Target,   isReadonly: boolean,   baseHandlers: ProxyHandler,   collectionHandlers: ProxyHandler,   proxyMap: WeakMap ) {   //如果不是对象,则警告,Proxy代理只支持对象   if (!isObject(target)) {     if (__DEV__) {       console.warn(`value cannot be made reactive: ${String(target)}`)     }     return target   }   //如果被代理对象已经是一个proxy对象且是响应式的并且此次创建的新代理对象不是只读的,则直接返回被代理对象   //这儿存在一种情况需要重新创建,即被代理对象已经是一个代理对象了,且可读可写。但新创建的代理对象是只读的   //那么,本次生成的那个代理对象最终是只读的。响应式必须可读可写,只读的代理对象是非响应式的。   if (     target[ReactiveFlags.RAW] &&     !(isReadonly && target[ReactiveFlags.IS_REACTIVE])   ) {     return target   }   //从map中找,如果对象已经被代理过,则直接从map中返回,否则生成代理   const existingProxy = proxyMap.get(target)   if (existingProxy) {     return existingProxy   }   // 获取代理类型,即采用集合类型的代理还是普通对象类型的代理   const targetType = getTargetType(target)   if (targetType === TargetType.INVALID) {     return target   }   // 生成代理对象并存入map中   const proxy = new Proxy(     target,     targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers   )   proxyMap.set(target, proxy)   return proxy }

2. 源码流程分析

Vue中创建响应式代理对象都是通过createReactiveObject方法创建。这个方法里面的主要逻辑很简单,就是生成一个目标对象的代理对象,代理对象最为核心的操作拦截则由外部根据是否只读和是否浅响应传入,然后将这个代理对象存起来以备下次快捷获取。

三、代理拦截操作

1. 数组操作

(1).关键源码

//源码位置: /packages/reactivity/src/baseHandlers.ts function createArrayInstrumentations() {   const instrumentations: Record = {}   // instrument identity-sensitive Array methods to account for possible reactive   // values   ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {     instrumentations[key] = function (this: unknown[], ...args: unknown[]) {       //获取原始数组       const arr = toRaw(this) as any       for (let i = 0, l = this.length; i < l; i++) {         //收集依赖 键值为索引 i         track(arr, TrackOpTypes.GET, i + '')       }       // 调用数组的原始方法       const res = arr[key](...args)       if (res === -1 || res === false) {         // 如果不存在,则将参数参数转换为原始数据在试一次(这儿可能是防止传入的是代理对象导致获取失败)         return arr[key](...args.map(toRaw))       } else {         return res       }     }   })   // instrument length-altering mutation methods to avoid length being tracked   // which leads to infinite loops in some cases (#2137)   ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {     instrumentations[key] = function (this: unknown[], ...args: unknown[]) {       //由于上面的方法会改变数组长度,因此暂停收集依赖,不然会导致无限递归       pauseTracking()       //调用原始方法       const res = (toRaw(this) as any)[key].apply(this, args)       //复原依赖收集       resetTracking()       return res     }   })   return instrumentations }

(2).源码流程分析

上述源码其实就是重写了对于数组方法的操作,在通过数组的代理对象访问以上数组方法时,就会执行重写后的数组方法。

内部逻辑很简单,对于改变了数组长度的方法,先暂停依赖收集,调用原始数组方法,然后复原依赖收集。

对于判断元素是否存在的数组方法,执行依赖收集并调用数组原始方法。

总结来说最终都是调用了数组的原始方法,只不过在调用前后添加了关于依赖收集相关的行为。

2.Get操作

(1).关键源码

//源码位置: /packages/reactivity/src/baseHandlers.ts /**  * 创建并且返回一个Get方法  * @param isReadonly 是否只读  * @param shallow 是否浅响应  * @returns   */ function createGetter(isReadonly = false, shallow = false) {   return function get(target: Target, key: string | symbol, receiver: object) {     //这儿不重要,其实就是通过代理对象访问这几个特殊属性时,返回相应的值,和响应式无关     if (key === ReactiveFlags.IS_REACTIVE) {       return !isReadonly     } else if (key === ReactiveFlags.IS_READONLY) {       return isReadonly     } else if (       key === ReactiveFlags.RAW &&       receiver ===         (isReadonly           ? shallow             ? shallowReadonlyMap             : readonlyMap           : shallow           ? shallowReactiveMap           : reactiveMap         ).get(target)     ) {       return target     }        const targetIsArray = isArray(target)     //如果是调用的数组方法,则调用重写后的数组方法,前提不是只读的     if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {       return Reflect.get(arrayInstrumentations, key, receiver)     }     //调用原始行为获取值     const res = Reflect.get(target, key, receiver)     //访问Symbol对象上的属性和__proto__,__v_isRef,__isVue这3个属性,直接返回结果值     if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {       return res     }     if (!isReadonly) {       //不是只读,则收集依赖       track(target, TrackOpTypes.GET, key)     }     if (shallow) {       //如果对象是浅响应的 则返回结果       return res     }     if (isRef(res)) {       //如果值是Ref对象且是通过数组代理对象的下标访问的,则不做解包装操作,否则返回解包装后的值       // ref unwrapping - does not apply for Array + integer key.       const shouldUnwrap = !targetIsArray || !isIntegerKey(key)       return shouldUnwrap ? res.value : res     }     if (isObject(res)) {       //走到这儿需要满足非浅响应。如果结果是一个对象,则将改对象转换为只读代理对象或者响应式代理对象返回       //e.g.        // test:{       //   a:{       //     c:10       //   }       // }       //以上测试对象当访问属性a时,此时res是一个普通对象,如果不转换为代理对象,则对a.c的操作不会被拦截处理,导致无法响应式处理       // 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.       return isReadonly ? readonly(res) : reactive(res)     }          return res   } }

(2).源码流程分析

上述Get方法是在通过代理对象获取某一个值时触发的。流程很简单,就是对几个特殊属性做了特殊返回。

如果是数组方法,则调用重写后的数组方法,不是则调用原始行为获取值。

如果不是只读,则收集依赖,对返回结果进行判断特殊处理。其中最关键的地方在于收集依赖和将获取到的嵌套对象转换为响应式对象。

3. Set操作

(1).关键源码

//源码位置: /packages/reactivity/src/baseHandlers.ts /**  * 创建并返回一个Set方法  * @param shallow 是否浅响应  * @returns   */ function createSetter(shallow = false) {   return function set(     target: object,     key: string | symbol,     value: unknown,     receiver: object   ): boolean {     //获取改变之前的值     let oldValue = (target as any)[key]     if (!shallow) {       value = toRaw(value)       oldValue = toRaw(oldValue)       //对Ref类型值的特殊处理       //比较2个值,如果旧值是Ref对象,新值不是,则直接变Ref对象的value属性       if (!isArray(target) && isRef(oldValue) && !isRef(value)) {         //这儿看似没有触发依赖更新,其实Ref对象的value进行赋值会触发Ref对象的写操作,在那个操作里面会触发依赖更新         oldValue.value = value         return true       }     } else {       // in shallow mode, objects are set as-is regardless of reactive or not     }     const hadKey =       isArray(target) && isIntegerKey(key)         ? Number(key) < target.length         : hasOwn(target, key)     const result = Reflect.set(target, key, value, receiver)     // don't trigger if target is something up in the prototype chain of original     // 这个判断其实是处理一个代理对象的原型也是代理对象的情况,以下是测试代码     // let hiddenValue: any     // const obj = reactive<{ prop?: number }>({})     // const parent = reactive({     //   set prop(value) {     //     hiddenValue = value     //   },     //   get prop() {     //     return hiddenValue     //   }     // })     // Object.setPrototypeOf(obj, parent)     // obj.prop = 4     // 当存在上述情形,第一次设置值时,由于子代理没有prop属性方法,会触发父代理的set方法。父代理的这个判断此时是false,算是一个优化,避免2个触发更新     if (target === toRaw(receiver)) {       if (!hadKey) {         //触发add类型依赖更新         trigger(target, TriggerOpTypes.ADD, key, value)       } else if (hasChanged(value, oldValue)) {         //触发set类型依赖更新         trigger(target, TriggerOpTypes.SET, key, value, oldValue)       }     }     return result   } }

(2).源码流程分析

当设置时,首先对旧值是Ref类型对象做了个特殊处理,如果满足条件,则走Ref对象的set方法逻辑触发依赖更新。

否则根据是否存在key值,判断是新增属性,还是修改属性,触发不同类型的依赖更新。

之所以要区分依赖类型,是因为某些属性会连带别的属性更改,比如数组直接设置下标,会导致length的更改,这个时候需要收集length为键值的依赖,以便连带更新依赖的length属性的地方。

4. 其余行为拦截操作

(1).关键源码

//源码位置: /packages/reactivity/src/baseHandlers.ts /**  * delete操作符时触发  * @param target 目标对象  * @param key 键值  * @returns   */ function deleteProperty(target: object, key: string | symbol): boolean {   const hadKey = hasOwn(target, key)   const oldValue = (target as any)[key]   const result = Reflect.deleteProperty(target, key)   if (result && hadKey) {     //触发依赖更新     trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)   }   return result } /**  * in 操作符时触发  * @
                
                

-六神源码网