站长资讯网
最全最丰富的资讯网站

详解vue3中reactive和ref的区别(源码解析)

vue中reactive和ref的区别是什么?下面本篇文章带大家深入源码彻底搞清vue3中reactive和ref的区别,希望对大家有所帮助!

详解vue3中reactive和ref的区别(源码解析)

如何快速入门VUE3.0:进入学习

在vue3的日常开发中,我发现很多人都是基于自己的习惯reactiveref一把梭,虽然这样都可以实现需求,既然这样那为什么已经有了reactive还需要再去设计一个ref呢?这两者的实际运用场景以及区别是什么呢?

并且关于ref的底层逻辑,有的人说ref的底层逻辑还是reactive。有的人说ref的底层是classvalue只是这个class的一个属性,那这两种说法哪种正确呢?都有没有依据呢?

抱着这样的疑问我们本次就深入源码,彻底搞清vue3中reactiveref的区别。(学习视频分享:vue视频教程)

不想看源码的童鞋,可以直接拉到后面看总结

reactive

源码地址:packages/reactivity/reactive.ts

首先我们看一下vue3中用来标记目标对象target类型的ReactiveFlags

// 标记目标对象 target 类型的 ReactiveFlags export const enum ReactiveFlags {   SKIP = '__v_skip',   IS_REACTIVE = '__v_isReactive',   IS_READONLY = '__v_isReadonly',   RAW = '__v_raw' }  export interface Target {   [ReactiveFlags.SKIP]?: boolean          // 不做响应式处理的数据   [ReactiveFlags.IS_REACTIVE]?: boolean   // target 是否是响应式   [ReactiveFlags.IS_READONLY]?: boolean   // target 是否是只读   [ReactiveFlags.RAW]?: any               // 表示proxy 对应的源数据, target 已经是 proxy 对象时会有该属性 }

reactive

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> export function reactive(target: object) {   // if trying to observe a readonly proxy, return the readonly version.   // 如果目标对象是一个只读的响应数据,则直接返回目标对象   if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {     return target   }   // 创建 observe   return createReactiveObject(     target,     false,     mutableHandlers,     mutableCollectionHandlers,     reactiveMap   ) }

reactive函数接收一个target对象,如果target对象只读则直接返回该对象

若非只读则直接通过createReactiveObject创建observe对象

createReactiveObject

看着长不要怕,先贴createReactiveObject完整代码,我们分段阅读

/**  *   * @param target 目标对象  * @param isReadonly 是否只读  * @param baseHandlers 基本类型的 handlers  * @param collectionHandlers 主要针对(set、map、weakSet、weakMap)的 handlers  * @param proxyMap  WeakMap数据结构  * @returns   */  function createReactiveObject(   target: Target,   isReadonly: boolean,   baseHandlers: ProxyHandler<any>,   collectionHandlers: ProxyHandler<any>,   proxyMap: WeakMap<Target, any> ) {    // typeof 不是 object 类型的,在开发模式抛出警告,生产环境直接返回目标对象   if (!isObject(target)) {     if (__DEV__) {       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   // 已经是响应式的就直接返回(取ReactiveFlags.RAW 属性会返回true,因为进行reactive的过程中会用weakMap进行保存,   // 通过target能判断出是否有ReactiveFlags.RAW属性)   // 例外:对reactive对象进行readonly()   if (     target[ReactiveFlags.RAW] &&     !(isReadonly && target[ReactiveFlags.IS_REACTIVE])   ) {     return target   }   // target already has corresponding Proxy   // 对已经Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象   const existingProxy = proxyMap.get(target)   if (existingProxy) {     return existingProxy   }   // only a whitelist of value types can be observed.   // 只对targetTypeMap类型白名单中的类型进行响应式处理   const targetType = getTargetType(target)   if (targetType === TargetType.INVALID) {     return target   }   // proxy 代理 target   // (set、map、weakSet、weakMap) collectionHandlers   // (Object、Array) baseHandlers   const proxy = new Proxy(     target,     targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers   )   proxyMap.set(target, proxy)   return proxy }

首先我们看到createReactiveObject接收了五个参数

  target: Target,   isReadonly: boolean,   baseHandlers: ProxyHandler<any>,   collectionHandlers: ProxyHandler<any>,   proxyMap: WeakMap<Target, any>

target 目标对象

isReadonly 是否只读

baseHandlers 基本类型的 handlers 处理数组,对象

collectionHandlers 处理 set、map、weakSet、weakMap

proxyMap WeakMap数据结构存储副作用函数


这里主要是通过ReactiveFlags.RAWReactiveFlags.IS_REACTIVE判断是否是响应式数据,若是则直接返回该对象

 if (     target[ReactiveFlags.RAW] &&     !(isReadonly && target[ReactiveFlags.IS_REACTIVE])   ) {     return target   }

对于已经是Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象并返回

  const existingProxy = proxyMap.get(target)   if (existingProxy) {     return existingProxy   }

这里则是校验了一下当前target的类型是不是ObjectArrayMapSetWeakMapWeakSet,如果都不是则直接返回该对象,不做响应式处理

 // 只对targetTypeMap类型白名单中的类型进行响应式处理   const targetType = getTargetType(target)   if (targetType === TargetType.INVALID) {     return target   }

校验类型的逻辑

function getTargetType(value: Target) {   return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)     ? TargetType.INVALID     : targetTypeMap(toRawType(value)) }  function targetTypeMap(rawType: string) {   switch (rawType) {     case 'Object':     case 'Array':       return TargetType.COMMON     case 'Map':     case 'Set':     case 'WeakMap':     case 'WeakSet':       return TargetType.COLLECTION     default:       return TargetType.INVALID   } }

所有的前置校验完后,就可以使用proxy 代理target对象了

这里使用了一个三目运算符通过TargetType.COLLECTION来执行不同的处理逻辑

  • (set、map、weakSet、weakMap) 使用 collectionHandlers
  • (Object、Array) 使用 baseHandlers
// proxy 代理 target   // (set、map、weakSet、weakMap) collectionHandlers   // (Object、Array) baseHandlers   const proxy = new Proxy(     target,     targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers   )   proxyMap.set(target, proxy)   return proxy

现在对createReactiveObject的执行逻辑是不是就很清晰了

到这里还没有结束,createReactiveObject中最后proxy是如何去代理target的呢?这里我们用baseHandlers举例,深入baseHandlers的内部去看看

baseHandlers

源码地址:packages/reactivity/baseHandlers.ts

reactive.ts中我们可以看到一共引入了四种 handler

import {   mutableHandlers,   readonlyHandlers,   shallowReactiveHandlers,   shallowReadonlyHandlers } from './baseHandlers'
  • mutableHandlers 可变处理
  • readonlyHandlers 只读处理
  • shallowReactiveHandlers 浅观察处理(只观察目标对象的第一层属性)
  • shallowReadonlyHandlers 浅观察 && 只读

我们以mutableHandlers为例

// 可变处理 // const get = /*#__PURE__*/ createGetter() // const set = /*#__PURE__*/ createSetter() // get、has、ownKeys 会触发依赖收集 track() // set、deleteProperty 会触发更新 trigger() export const mutableHandlers: ProxyHandler<object> = {   get,                  // 用于拦截对象的读取属性操作   set,                  // 用于拦截对象的设置属性操作   deleteProperty,       // 用于拦截对象的删除属性操作   has,                  // 检查一个对象是否拥有某个属性   ownKeys               // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法 }

这里的getset分别对应着createGetter()createSetter()

  • createGetter()

先上完整版代码

/**  * 用于拦截对象的读取属性操作  * @param isReadonly 是否只读  * @param shallow 是否浅观察  * @returns   */ function createGetter(isReadonly = false, shallow = false) {   /**    * @param target 目标对象    * @param key 需要获取的值的键值    * @param receiver 如果遇到 setter,receiver 则为setter调用时的this值    */   return function get(target: Target, key: string | symbol, receiver: object) {     // ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值     if (key === ReactiveFlags.IS_REACTIVE) {       return !isReadonly     } else if (key === ReactiveFlags.IS_READONLY) {       return isReadonly     } else if (       // 如果key是raw  receiver 指向调用者,则直接返回目标对象。       // 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者       // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找       key === ReactiveFlags.RAW &&       receiver ===         (isReadonly           ? shallow             ? shallowReadonlyMap             : readonlyMap           : shallow           ? shallowReactiveMap           : reactiveMap         ).get(target)     ) {       return target     }      const targetIsArray = isArray(target)     // 如果目标对象 不为只读、是数组、key属于arrayInstrumentations:['includes', 'indexOf', 'lastIndexOf']方法之一,即触发了这三个方法之一     if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {       // 通过 proxy 调用,arrayInstrumentations[key]的this一定指向 proxy       return Reflect.get(arrayInstrumentations, key, receiver)     }      const res = Reflect.get(target, key, receiver)      // 如果 key 是 symbol 内置方法,或者访问的是原型对象__proto__,直接返回结果,不收集依赖     if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {       return res     }      // 不是只读类型的 target 就收集依赖。因为只读类型不会变化,无法触发 setter,也就会触发更新     if (!isReadonly) {       track(target, TrackOpTypes.GET, key)     }      // 如果是浅观察,不做递归转化,就是说对象有属性值还是对象的话不递归调用 reactive()     if (shallow) {       return res     }      // 如果get的结果是ref     if (isRef(res)) {       // ref unwrapping - does not apply for Array + integer key.       // 返回 ref.value,数组除外       const shouldUnwrap = !targetIsArray || !isIntegerKey(key)       return shouldUnwrap ? res.value : res     }      // 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理     if (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.       return isReadonly ? readonly(res) : reactive(res)     }      return res   } }

看着长,最终就是track()依赖收集

track()依赖收集内容过多,和trigger()触发更新一起,单开一篇文章

  • createSetter()

/**  * 拦截对象的设置属性操作  * @param shallow 是否是浅观察  * @returns   */ function createSetter(shallow = false) {   /**    * @param target 目标对象    * @param key 设置的属性名称    * @param value 要改变的属性值    * @param receiver 如果遇到setter,receiver则为setter调用时的this值    */   return function set(     target: object,     key: string | symbol,     value: unknown,     receiver: object   ): boolean {     let oldValue = (target as any)[key]     // 如果模式不是浅观察模式     if (!shallow) {       // 拿新值和老值的原始值,因为新传入的值可能是响应式数据,如果直接和 target 上原始值比较是没有意义的       value = toRaw(value)       oldValue = toRaw(oldValue)       // 目标对象不是数组,旧值是ref,新值不是ref,则直接赋值,这里提到ref       if (!isArray(target) && isRef(oldValue) && !isRef(value)) {         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     // reactive是proxy实例才触发更新,防止通过原型链触发拦截器触发更新     if (target === toRaw(receiver)) {       if (!hadKey) {         // 如果不存在则trigger ADD         trigger(target, TriggerOpTypes.ADD, key, value)       } else if (hasChanged(value, oldValue)) {         // 如果新旧值不相等则trigger SET         trigger(target, TriggerOpTypes.SET, key, value, oldValue)       }     }     return result   } }

trigger()触发更新

ref

源码地址:packages/reactivity/src/ref.ts

接收一个可选unknown,接着直接调用createRef()

export function ref(value?: unknown) {   return createRef(value, false) }

详解vue3中reactive和ref的区别(源码解析)

ref的区别就是在调用createRef()时第二个值传的是true

export function shallowRef(value?: unknown) {   return createRef(value, true) }

看一下官方文档上对shallowRef的解释

详解vue3中reactive和ref的区别(源码解析)

createRef

通过isRef()判断是否是ref数据,是则直接返回该数据,不是则通过new RefImpl创建ref数据

在创建时会传两个值一个是rawValue(原始值),一个是shallow(是否是浅观察),具体使用场景可看上面refshallowRef的介绍

function createRef(rawValue: unknown, shallow: boolean) {   // 是否是 ref 数据   if (isRef(rawValue)) {     return rawValue   }   return new RefImpl(rawValue, shallow) }
  • isRef()

通过__v_isRef只读属性判断是否是ref数据,此属性会在RefImpl创建ref数据时添加

export function isRef(r: any): r is Ref {   return Boolean(r && r.__v_isRef === true) }

RefImpl

class RefImpl<T> {   private _value: T   private _rawValue: T    public dep?: Dep = undefined   // 只读属性 __v_isRef 判断是否是ref数据的静态标识   public readonly __v_isRef = true    constructor(value: T, public readonly _shallow: boolean) {     this._rawValue = _shallow ? value : toRaw(value)  // 非浅观察用toRaw()包裹原始值     this._value = _shallow ? value : toReactive(value) // 非浅观察用toReactive()处理数据   }    get value() {   // 依赖收集     trackRefValue(this)     return this._value   }    set value(newVal) {     newVal = this._shallow ? newVal : toRaw(newVal) // 非浅观察用toRaw()包裹值     // 两个值不相等     if (hasChanged(newVal, this._rawValue)) {       this._rawValue = newVal       this._value = this._shallow ? newVal : toReactive(newVal)       triggerRefValue(this, newVal) // 触发依赖,派发更新     }   } }

根据RefImpl我们可以看到ref的底层逻辑,如果是对象确实会使用reactive进行处理,并且ref的创建使用的也是RefImpl class实例,value只是RefImpl的属性

在我们访问设置 ref的value值时,也分别是通过getset拦截进行依赖收集派发更新

  • toReactive

我们来看一下toReactive()这个方法,在RefImpl中创建ref数据时会调用toReactive()方法,这里会先判断传进来的值是不是对象,如果是就用reactive()包裹,否则就返回其本身

export const toReactive = <T extends unknown>(value: T): T =>   isObject(value) ? reactive(value) : value
  • trackRefValue

ref的依赖收集方法

export function trackRefValue(ref: RefBase<any>) {   if (isTracking()) {     ref = toRaw(ref)     if (!ref.dep) {       ref.dep = createDep()     }     if (__DEV__) {       trackEffects(ref.dep, {         target: ref,         type: TrackOpTypes.GET,         key: 'value'       })     } else {       trackEffects(ref.dep)     }   } }
  • triggerRefValue

ref的派发更新方法

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {   ref = toRaw(ref)   if (ref.dep) {     if (__DEV__) {       triggerEffects(ref.dep, {         target: ref,         type: TriggerOpTypes.SET,         key: 'value',         newValue: newVal       })     } else {       triggerEffects(ref.dep)     }   } }

总结

看完reactiveref源码,相信对本文一开始的几个问题也都有了答案,这里也总结了几个问题:

  • 问:ref的底层逻辑是什么,具体是如何实现的

答:ref底层会通过 new RefImpl()来创造ref数据,在new RefImpl()会首先给数据添加__v_isRef只读属性用来标识ref数据。而后判断传入的值是否是对象,如果是对象则使用toReactive()处理成reactive,并将值赋给RefImpl()value属性上。在访问设置ref数据的value时会分别触发依赖收集派发更新流程。


  • 问:ref底层是否会使用reactive处理数据

答:RefImpl中非浅观察会调用toReactive()方法处理数据,toReactive()中会先判断传入的值是不是一个对象,如果是对象则使用reactive进行处理,不是则直接返回值本身。


  • 问:为什么已经有了reactive还需要在设计一个ref呢?

答: 因为vue3响应式方案使用的是proxy,而proxy的代理目标必须是非原始值,没有任何方式能去拦截对原始值的操作,所以就需要一层对象作为包裹,间接实现原始值的响应式方案。


  • 问:为什么ref数据必须要有个value属性,访问ref数据必须要通过.value的方式呢?

答:这是因为要解决响应式丢失的问题,举个例子:

// obj是响应式数据 const obj = reactive({ foo: 1, bar: 2 })  // newObj 对象下具有与 obj对象同名的属性,并且每个属性值都是一个对象 // 该对象具有一个访问器属性 value,当读取 value的值时,其实读取的是 obj 对象下相应的属性值  const newObj = {     foo: {         get value() {             return obj.foo         }     },     bar: {         get value() {             return obj.bar         }     } }  effect(() => {     // 在副作用函数内通过新对象 newObj 读取 foo 的属性值     console.log(newObj.foo) }) // 正常触发响应 obj.foo = 100

可以看到,在现在的newObj对象下,具有与obj对象同名的属性,而且每个属性的值都是一个对象,例如foo 属性的值是:

{     get value() {         return obj.foo     } }

该对象有一个访问器属性value,当读取value的值时,最终读取的是响应式数据obj下的同名属性值。也就是说,当在副作用函数内读取newObj.foo时,等价于间接读取了obj.foo的值。这样响应式数据就能够与副作用函数建立响应联系

(学习视频分享:web前端开发、编程基础视频)

赞(0)
分享到: 更多 (0)
网站地图   沪ICP备18035694号-2    沪公网安备31011702889846号