Vue3 Reactive及其衍生函数 源码探析

简介: Vue3 Reactive及其衍生函数 源码探析

源码位置:package/reactivity/src/reactive ,另外笔者在代码中去除了一些边界代码

src下的 baseHandlers 导出数据类型为数组 对象的proxy配置对象
src下的 collectionHandlers 导出数据类型为set map weakSet weakMap的proxy配置对象

这篇文章 介绍了Proxy以及Vue是如何代理不同数据类型

对于reactive ShallowReactive readonly shallowReadonly的核心都是createReactiveObject(工厂函数),这四个函数只需要负责接收参数,再将参数传递给createReactiveObject就可以返回正确的代理对象

接着让我们进入createReactiveObject,下面的代码里给出了详细注释.

另外笔者在阅读的时候遇到一个问题,这里分享给大家:

ReactiveFlags是一个枚举类型,因此ReactiveFlags.RAW可以拿到对应的key
一开始我以为它是在某个地方给target设置了这么一个key属性.比如代理后给原来的target做个标记以区别是否代理过. 但是在源码里死活找不到在哪里赋的值

然而真相是...... 这个属性的读取会被proxy拦截

  1. 如果target是一个未被代理的普通对象,肯定没有ReactiveFlags.RAW属性,判断条件为假,不会直接返回target.
  2. 如果target已经被代理,找到proxy中的get操作,如果访问的key是ReactiveFlags.RAW,会返回原始对象target.访问的key是ReactiveFlags.IS_REACTIVE,isReadonly是只读的标志,也就是标识是否是只读的代理对象.
//reactive 
export const enum ReactiveFlags {
   
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

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

  //baseHandlers
   if (key === ReactiveFlags.IS_REACTIVE) {
   
      return !isReadonly
    }
   else if (
      key === ReactiveFlags.RAW 
    ){
   
      return target
    }
function createReactiveObject(  
  target: Target,//被代理对象  
  isReadonly: boolean,//是否只读  
  baseHandlers: ProxyHandler<any>,//数据类型为数组,对象时的proxy配置对象  
  collectionHandlers: ProxyHandler<any>,//数据类型为set map weakSet weakMap的proxy配置对象  
  proxyMap: WeakMap<Target, any>//代理对象与被代理对象的联系存储在weakMap,
  //对于浅响应,深响应,浅只读,深只读都有自己的weakMap,  
  //所以对于一个数据,可以建立它的深浅响应,深浅只读代理对象。  
) {
     
  //判断是否是对象,typeof set === 'object'    
  if (!isObject(target)) {
     
    if (__DEV__) {
     
      console.warn(`value cannot be made reactive: ${
     String(target)}`)  
    }  
    return target  
  }  
  // 如果target本身是一个代理,并且
  // 除了 这个代理对象是不是只读对象并调用readonly,其他情况不需要处理,直接返回代理对象
  //exception: calling readonly() on a reactive object  
  if (  
    target[ReactiveFlags.RAW] &&  
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])  
  ) {
     
    return target  
  }  
  //判断target是否被代理过,如果代理过会存在 `proxyMap`
  const existingProxy = proxyMap.get(target)  
  if (existingProxy) {
     
    return existingProxy  
  }  
  // 除了上面提到的数据类型都不能被代理  
  const targetType = getTargetType(target)  
  if (targetType === TargetType.INVALID) {
     
    return target  
  }  
  //proxy的第二个参数是一个配置对象可以包含一组捕获器(handler traps),  
  //这些捕获器定义了在代理对象上触发的各种操作的行为。  
  //每个捕获器都是一个函数,负责处理相应的操作。  
  const proxy = new Proxy(  
    target,  
    //如果被代理对象是数组或者对象使用baseHandlers,set map weakSet weakMap使用collectionHandlers作为配置  
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers  
  )  
  //最后将传人的proxyMap设置一下
  proxyMap.set(target, proxy)  
  //返回代理对象  
  return proxy  
}

紧接着,再来看看proxy的第二个参数即配置对象,看看这四个函数分别传递了什么作为配置对象

  1. 在下面代码里注释了配置对象里不同方法的用途
  2. 可以看到ShallowReactive共用了reactive的配置对象,并在重写了get set,shallowReadonly也是共用了readonly的配置对象,重写了get
  3. 对于只读对象来说,其实只需要拦截get即可,这里还加上set,deleteProperty只是为了展示错误提示.
//reactive
export const mutableHandlers: ProxyHandler<object> = {
   
  get,//拦截属性读取
  set,//拦截设置属性
  deleteProperty,//拦截删除属性
  has,//拦截`in`,(key in obj)
  ownKeys//拦截 for in 循环
}
//ShallowReactive
export const shallowReactiveHandlers = /*#__PURE__*/ extend(
  {
   },
  mutableHandlers,
  {
   
    get: shallowGet,
    set: shallowSet
  }
)
//readonly
export const readonlyHandlers: ProxyHandler<object> = {
   
  get: readonlyGet,
  set(target, key) {
   
    if (__DEV__) {
   
      warn(
        `Set operation on key "${
     String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
   
    if (__DEV__) {
   
      warn(
        `Delete operation on key "${
     String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}
//shallowReadonly
export const shallowReadonlyHandlers = /*#__PURE__*/ extend(
  {
   },
  readonlyHandlers,
  {
   
    get: shallowReadonlyGet
  }
)

下面分别介绍了这几个捕获器的实现流程

get

先来看看这四个函数的get是怎么创建的,可以看出它也是被一个工厂函数createGetter创建的.
让我们再进入createGetter函数,可以知道他根据接收的参数返回合适的get

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

可以看到get中果然拦截了key为ReactiveFlags.IS_REACTIVE等等,awesome,这与上面的内容就对上了.

function createGetter(isReadonly = false, shallow = false) {
   
  return function get(target: Target, key: string | symbol, receiver: object) {
   
    if (key === ReactiveFlags.IS_REACTIVE) {
   
      return !isReadonly
    } 
    ..........
    }}

接着判断是数组,需要 重写数组方法

  1. 代理对象设置为只读,不需要使用重写后的方法
  2. hasOwn ,它用于检查对象是否具有指定的属性.也就是代理的是数组并且调用的方法是重写的方法之一,改为调用重写之后的方法.
  3. 下面这个判断为了解决一个bug,当访问hasOwnProperty会收集依赖,但是proxyData.hasOwnProperty(key)里面这个key也应该收集依赖.
  const targetIsArray = isArray(target)


    if (!isReadonly) {
   
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
   
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
   
        return hasOwnProperty
      }
    }

    function hasOwnProperty(this: object, key: string) {
   
      const obj = toRaw(this)
      track(obj, TrackOpTypes.HAS, key)
      return obj.hasOwnProperty(key)
}

isObject就是利用里typeof进行判断并排除了null的情况

  1. 如果是只读,代表数据变化不需要做出任何响应,也就是不用收集依赖.
  2. 如果是浅处理,直接返回res.否则应该返回reactive或者readonly处理过后的res
  3. 这里与Vue2相比,不会一次性把对象变成深响应.而是只有访问到这个属性才会给它添加响应性.因为proxy可以监听到数据读取.这个时候给他添加上响应性

    const res = Reflect.get(target, key, receiver)
    
    if (!isReadonly) {
         
       track(target, TrackOpTypes.GET, key)
     }
    
    if (shallow) {
         
       return res
     }
    if (isObject(res)) {
         
       return isReadonly ? readonly(res) : reactive(res)
     }
    
     return res
    
    const isObject = (val: unknown): val is Record<any, any> =>
    val !== null && typeof val === 'object'
    

    set

    拦截修改操作,对属性进行修改

对于readonly,shallowReadyonly来说,在set捕获器中只需要抛出错误即可.
对于reactive,shallowReactive,它们的set是通过工厂函数createSetter生成

hadKey是判断新增属性还是修改属性的标记.当handKey为false,即新增.触发依赖,并将ADD传递给trigger

hasChanged这个函数判断新旧值是否相同,如果相同就没必要重新触发依赖.另外hasChanged里利用的是Object.is,它与全等的区别是能处理NAN

 const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)

      if (!hadKey) {
   
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
   
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }

    return result

deleteProperty

拦截修改操作,即删除属性.

如果删除的属性不属于target,或者删除失败,都不需要触发依赖(trigger)

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
}

has

拦截读取操作中的一种情况,判断对象或原型上是否存在给定的 key:key in obj。

如果key是一个symbol类型不进行依赖收集.因为利用for...of遍历数组还会读取数组的 Symbol.iterator 属性。该属性是一个 symbol值,没有必要建立响应联系

function has(target: object, key: string | symbol): boolean {
   
  const result = Reflect.has(target, key)
  if (!isSymbol(key)) {
   
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

ownKeys

为了拦截读取操作中的另一种情况,使用 for...in/of 循环遍历对象

直接收集依赖(track)即可,因为ownKeys参数没有key,所以用一个symbol类型的值来表示key.对应数组,可以直接把length当成key.

function ownKeys(target: object): (string | symbol)[] {
   
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

接下来让我们看看reactive.ts文件里还导出了什么函数

toReactive() toReadonly()

还是利用了reactive readonly,不再赘述

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

export const toReadonly = <T extends unknown>(value: T): T =>
  isObject(value) ? readonly(value) : value

markRaw()

将一个对象标记为不可被转为代理。返回该对象本身。

实现原理是给对象添加一个属性ReactiveFlags.SKIP用来标记不能代理.在createReactiveObject中会对这个属性进行判断,如果是true,直接返回原始对象.

export function markRaw<T extends object>(value: T): Raw<T> {
   
  def(value, ReactiveFlags.SKIP, true)
  return value
}

export const def = (obj: object, key: string | symbol, value: any) => {
   
  Object.defineProperty(obj, key, {
   
    configurable: true,
    enumerable: false,
    value
  })
}

isReactive() , isReadonly() , isShallow() , toRaw()

实现原理:都是在get捕获器中拦截ReactiveFlags里对应的key

export function isReactive(value: unknown): boolean {
   
  if (isReadonly(value)) {
   
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

export function isReadonly(value: unknown): boolean {
   
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

export function isShallow(value: unknown): boolean {
   
  return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
}

export function toRaw<T>(observed: T): T {
   
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

isProxy()

不是Reactive也不是isReadonly类型肯定就不是proxy了

export function isProxy(value: unknown): boolean {
   
  return isReactive(value) || isReadonly(value)
}

至此,reactive.ts中导出的函数都已经被罗列并分析了一遍.

可以看出reactive.ts文件的作用是为了实现对数据的代理,从'@vue/shared'导入了一些全局共享工具函数,从baseHandlers.ts和collectionHandlers.ts文件导入了关于proxy的配置对象.并导出了一些与代理相关的函数.

对于这两个文件,它们导入了effect模块,用来进行收集依赖,并派发通知.并且接收了reactive.ts中的一些函数.

相关文章
|
14天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
117 64
|
14天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
24 8
|
13天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
17 1
|
13天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
25 1
|
14天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
17天前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
19天前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
|
20天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
23 1
vue学习第一章
|
20天前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
22 1
vue学习第三章
|
20天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
35 1
vue学习第四章