reactive

简介: 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中的一些函数.

相关文章
|
3月前
|
JavaScript
Vue3 : ref 与 reactive
Vue3 : ref 与 reactive
97 1
|
4月前
|
JavaScript API 开发者
Vue 3 为什么同时需要 Ref 和 Reactive?
Vue 3 为什么同时需要 Ref 和 Reactive?
|
4月前
|
JavaScript 前端开发 数据管理
|
7月前
|
JavaScript 前端开发
【Vue3】toRefs和toRef在reactive中的一些应用
【Vue3】toRefs和toRef在reactive中的一些应用
|
7月前
|
API
Vue3中的ref和shallowRef、reactive和shallowReactive
Vue3中的ref和shallowRef、reactive和shallowReactive
182 1
|
7月前
|
JavaScript API
vue3ref和reactive
vue3ref和reactive
41 0
|
7月前
|
JavaScript 前端开发
详解vue3的ref和reactive
详解vue3的ref和reactive
146 0
详解vue3的ref和reactive
|
7月前
Vue3的reactive、ref、toRef、toRefs用法以及区别
Vue3的reactive、ref、toRef、toRefs用法以及区别
114 0
|
7月前
|
JavaScript API
vue3使用ref和reactive
vue3使用ref和reactive
|
JavaScript
reactive
reactive