乐趣区

关于vue.js:听说你很了解-Vue3-响应式

前言

<< 舒适揭示 >> 本文内容偏干,倡议边喝水边食用,如有不适请及时点赞!

【A】:能不能说说 Vue3 响应式都解决了哪些数据类型?都怎么解决的呀?

【B】:能,只能说一点点...

【A】:...

只有问到 Vue 相干的内容,仿佛总绕不过 响应式原理 的话题,随之而来的答复必然是围绕着 Object.definePropertyProxy 来开展(即 Vue2Vue3),但若持续诘问某些具体实现是不是就仓促完结答复了( 你跑我追,你不跑我还追)。

本文就不再过多介绍 Vue2 中响应式的解决,感兴趣能够参考 从 vue 源码看问题 —— 如何了解 Vue 响应式?,然而会有简略提及,上面就来看看 Vue3 中是如何解决 原始值、Object、Array、Set、Map 等数据类型的响应式。

Object.definePropertyProxy

所有的所有还得从 Object.defineProperty 开始讲起,那是一个不一样的 API …(bgm 响起,自行领会

Object.defineProperty

Object.defineProperty(obj, prop, descriptor) 办法会间接在一个对象上定义一个 新属性 ,或批改一个 对象 现有属性,并返回此对象,其参数具体为:

  • obj:要定义属性的对象
  • prop:要定义或批改的 属性名称 或 Symbol 
  • descriptor:要定义或批改的 属性描述符

从以上的形容就能够看出一些限度,比方:

  • 指标是 对象属性 ,不是 整个对象
  • 一次只能 定义或批改一个属性

    • 当然有对应的一次解决多个属性的办法 Object.defineProperties(),但在 vue 中并不实用,因为 vue 不能提前晓得用户传入的对象都有什么属性,因而还是得通过相似 Object.keys() + for 循环的形式获取所有的 key -> value,而这其实是没有必要应用 Object.defineProperties()

在 Vue2 中的缺点

Object.defineProperty() 理论是通过 定义 批改 对象属性 的描述符来实现 数据劫持,其对应的毛病也是没法被疏忽的:

  • 只能拦挡对象属性的 getset 操作,比方无奈拦挡 deletein办法调用 等操作
  • 动静增加新属性(响应式失落)

    • 保障后续应用的属性要在初始化申明 data 时进行定义
    • 应用 this.$set() 设置新属性
  • 通过 delete 删除属性(响应式失落)

    • 应用 this.$delete() 删除属性
  • 应用数组索引 替换 / 新增 元素(响应式失落)

    • 应用 this.$set() 设置新元素
  • 应用数组 push、pop、shift、unshift、splice、sort、reverse原生办法 扭转原数组时(响应式失落)

    • 应用 重写 / 加强 后的 push、pop、shift、unshift、splice、sort、reverse 办法
  • 一次只能对一个属性实现 数据劫持,须要遍历对所有属性进行劫持
  • 数据结构简单时(属性值为 援用类型数据 ),须要通过 递归 进行解决

【扩大】Object.definePropertyArray

它们有啥关系,其实没有啥关系,只是大家习惯性的会答复 Object.defineProperty 不能拦挡 Array 的操作,这句话说得对但也不对。

应用 Object.defineProperty 拦挡 Array

Object.defineProperty 可用于实现对象属性的 getset 拦挡,而数组其实也是对象,那天然是能够实现对应的拦挡操作,如下:

Vue2 为什么不应用 Object.defineProperty 拦挡 Array?

尤大在曾在 GitHubIssue 中做过如下回复:

说实话性能问题到底指的是什么呢?上面是总结了一些目前看到过的答复:

  • 数组 和 一般对象 在应用场景下有区别 ,在我的项目中应用数组的目标大多是为了 遍历,即比拟少会应用 array[index] = xxx 的模式,更多的是应用数组的 Api 的形式
  • 数组长度是多变的,不可能像一般对象一样先在 data 选项中提前申明好所有元素,比方通过 array[index] = xxx 形式赋值时,一旦 index 的值超过了现有的最大索引值,那么以后的增加的新元素也不会具备响应式
  • 数组存储的元素比拟多,不可能为每个数组元素都设置 getter/setter
  • 无奈拦挡数组原生办法如 push、pop、shift、unshift 等的调用 ,最终仍需 重写 / 加强 原生办法

Proxy & Reflect

因为在 Vue2 中应用 Object.defineProperty 带来的缺点,导致在 Vue2 中不得不提供了一些额定的办法(如:Vue.set、Vue.delete())解决问题,而在 Vue3 中应用了 Proxy 的形式来实现 数据劫持,而上述的问题在 Proxy 中都能够失去解决。

Proxy

Proxy 次要用于创立一个 对象的代理 ,从而实现基本操作的拦挡和自定义(如属性查找、赋值、枚举、函数调用等),实质上是通过拦挡对象 外部办法 的执行实现代理,而对象自身依据标准定义的不同又会辨别为 惯例对象 异质对象(这不是重点,可自行理解)。

  • new Proxy(target, handler) 是针对整个对象进行的代理,不是某个属性
  • 代理对象属性领有 读取、批改、删除、新增、是否存在属性 等操作相应的捕获器, 更多可见

    • get() 属性 读取 操作的捕获器
    • set() 属性 设置 操作的捕获器
    • deleteProperty()delete 操作符的捕获器
    • ownKeys()Object.getOwnPropertyNames 办法和 Object.getOwnPropertySymbols 办法的捕获器
    • has()in 操作符的捕获器

Reflect

Reflect 是一个内置的对象,它提供拦挡 JavaScript 操作的办法,这些办法与 Proxy handlers“) 提供的的办法是一一对应的,且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和办法都是动态的。

  • Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,相似于 target[name]
  • Reflect.set(target, propertyKey, value[, receiver]) 将值调配给属性的函数。返回一个Boolean,如果更新胜利,则返回true
  • Reflect.deleteProperty(target, propertyKey) 作为函数的 delete 操作符,相当于执行 delete target[name]
  • Reflect.ownKeys(target) 返回一个蕴含所有本身属性(不蕴含继承属性)的数组。(相似于 Object.keys(), 但不会受enumerable 影响)
  • Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的性能完全相同

更多办法点此可见

Proxy 为什么须要 Reflect 呢?

Proxyget(target, key, receiver)、set(target, key, newVal, receiver) 的捕捉器中都能接到后面所列举的参数:

  • target 指的是 原始数据对象
  • key 指的是以后操作的 属性名
  • newVal 指的是以后操作接管到的 最新值
  • receiver 指向的是以后操作 正确的上下文
怎么了解 Proxy handlerreceiver 指向的是以后操作正确上的下文呢?
  • 失常状况下,receiver 指向的是 以后的代理对象

  • 非凡状况下,receiver 指向的是 引发以后操作的对象

    • 通过 Object.setPrototypeOf() 办法将代理对象 proxy 设置为一般对象 obj 的原型
    • 通过 obj.name 拜访其不存在的 name 属性,因为原型链的存在,最终会拜访到 proxy.name 上,即触发 get 捕捉器

Reflect 的办法中通常只须要传递 target、key、newVal 等,但为了可能解决上述提到的非凡状况,个别也须要传递 receiver 参数,因为 Reflect 办法中传递的 receiver 参数代表执行原始操作时的 this 指向,比方:Reflect.get(target, key , receiver)Reflect.set(target, key, newVal, receiver)

总结 Reflect 是为了在执行对应的拦挡操作的办法时能 传递正确的 this 上下文

Vue3 如何应用 Proxy 实现数据劫持?

Vue3 中提供了 reactive()ref() 两个办法用来将 指标数据 变成 响应式数据 ,而通过 Proxy 来实现 数据劫持(或代理) 的具体实现就在其中,上面一起来看看吧!

reactive 函数

从源码来看,其外围其实就是 createReactiveObject(...) 函数,那么持续往下查看对应的内容

源码地位:packages\reactivity\src\reactive.ts

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 若指标对象是响应式的只读数据,则间接返回
  if (isReadonly(target)) {return target}

  // 否则将指标数据尝试变成响应式数据
  return createReactiveObject(
    target,
    false,
    mutableHandlers, // 对象类型的 handlers
    mutableCollectionHandlers, // 汇合类型的 handlers
    reactiveMap
  )
}

createReactiveObject() 函数

源码的体现也是非常简单,无非就是做一些前置判断解决:

  • 若指标数据是 原始值类型 ,间接向返回 原数据
  • 若指标数据的 __v_raw 属性为 true,且是【非响应式数据】或 不是通过调用 readonly() 办法,则间接返回 原数据
  • 若指标数据已存在相应的 proxy 代理对象,则间接返回 对应的代理对象
  • 若指标数据不存在对应的 白名单数据类型 中,则间接返回原数据,反对响应式的数据类型如下:

    • 可扩大的对象,即是否能够在它下面增加新的属性
    • __v_skip 属性不存在或值为 false 的对象
    • 数据类型为 Object、Array、Map、Set、WeakMap、WeakSet 的对象
    • 其余数据都对立被认为是 有效的响应式数据对象
  • 通过 Proxy 创立代理对象,依据指标数据类型抉择不同的 Proxy handlers

看来具体的实现又在不同数据类型的 捕捉器 中,即上面源码的 collectionHandlersbaseHandlers ,而它们则对应的是在上述 reactive() 函数中为 createReactiveObject() 函数传递的 mutableCollectionHandlersmutableHandlers 参数。

源码地位:packages\reactivity\src\reactive.ts

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {

  // 非对象类型间接返回
  if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // 指标数据的 __v_raw 属性若为 true,且是【非响应式数据】或 不是通过调用 readonly() 办法,则间接返回
  if (target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {return target}

  // 指标对象已存在相应的 proxy 代理对象,则间接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {return existingProxy}

  // 只有在白名单中的值类型才能够被代理监测,否则间接返回
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {return target}

  // 创立代理对象
  const proxy = new Proxy(
    target,
    // 若指标对象是汇合类型(Set、Map)则应用汇合类型对应的捕捉器,否则应用根底捕捉器
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers 
  )

  // 将对应的代理对象存储在 proxyMap 中
  proxyMap.set(target, proxy)

  return proxy
}

捕捉器 Handlers

对象类型的捕捉器 — mutableHandlers

这里的对象类型指的是 数组 一般对象

源码地位:packages\reactivity\src\baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

以上这些捕捉器其实就是咱们在上述 Proxy 局部列举进去的捕捉器,显然能够拦挡对一般对象的如下操作:

  • 读取,如 obj.name
  • 设置,如 obj.name = 'zs'
  • 删除属性,如 delete obj.name
  • 判断是否存在对应属性,如 name in obj
  • 获取对象本身的属性值,如 obj.getOwnPropertyNames()obj.getOwnPropertySymbols()
get 捕捉器

具体信息在上面的正文中,这里只列举核心内容:

  • 若以后数据对象是 数组 ,则 重写 / 加强 数组对应的办法

    • 数组元素的 查找办法includes、indexOf、lastIndexOf
    • 批改原数组 的办法:push、pop、unshift、shift、splice
  • 若以后数据对象是 一般对象 ,且非 只读 的则通过 track(target, TrackOpTypes.GET, key) 进行 依赖收集

    • 若以后数据对象是 浅层响应 的,则间接返回其对应属性值
    • 若以后数据对象是 ref 类型的,则会进行 主动脱 ref
  • 若以后数据对象的属性值是 对象类型

    • 若以后属性值属于 只读的,则通过 readonly(res) 向外返回其后果
    • 否则会将以后属性值以 reactive(res) 向外返回 proxy 代理对象
    • 否则间接向外返回对应的 属性值
    function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {
      // 当间接通过指定 key 拜访 vue 内置自定义的对象属性时,返回其对应的值
      if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow} 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)) {
        // 重写 / 加强数组的办法://  - 查找办法:includes、indexOf、lastIndexOf
        //  - 批改原数组的办法:push、pop、unshift、shift、splice
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
    
      // 获取对应属性值
      const res = Reflect.get(target, key, receiver)
    
      if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}
    
      // 依赖收集
      if (!isReadonly) {track(target, TrackOpTypes.GET, key)
      }
    
      // 浅层响应
      if (shallow) {return res}
    
      // 若是 ref 类型响应式数据,会进行【主动脱 ref】,但不反对【数组】+【索引】的拜访形式
      if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
        return shouldUnwrap ? res.value : res
      }
    
      // 属性值是对象类型://  - 是只读属性,则通过 readonly() 返回后果,//  - 且是非只读属性,则递归调用 reactive 向外返回 proxy 代理对象
      if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res)
      }
    
      return res
    }
    }
    set 捕捉器

    除去额定的边界解决,其实外围还是 更新属性值,并通过 trigger(...) 触发依赖更新

    function createSetter(shallow = false) {
    return function set(
      target: object,
      key: string | symbol,
      value: unknown,
      receiver: object
    ): boolean {
      // 保留旧的数据
      let oldValue = (target as any)[key]
    
      // 若原数据值属于 只读 且 ref 类型,并且新数据值不属于 ref 类型,则意味着批改失败
      if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false}
    
      if (!shallow && !isReadonly(value)) {if (!isShallow(value)) {value = toRaw(value)
          oldValue = toRaw(oldValue)
        }
        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}
    
      // 是否存在对应的 key
      const hadKey =
        isArray(target) && isIntegerKey(key)
          ? Number(key) < target.length
          : hasOwn(target, key)
    
      // 设置对应值
      const result = Reflect.set(target, key, value, receiver)
    
      // 若指标对象是原始原型链上的内容(非自定义增加),则不触发依赖更新
      if (target === toRaw(receiver)) {if (!hadKey) {
          // 指标对象不存在对应的 key,则为新增操作
          trigger(target, TriggerOpTypes.ADD, key, value)
        } else if (hasChanged(value, oldValue)) {
          // 指标对象存在对应的值,则为批改操作
          trigger(target, TriggerOpTypes.SET, key, value, oldValue)
        }
      }
    
      // 返回批改后果
      return result
    }
    }
deleteProperty & has & ownKeys 捕捉器

这三个捕捉器内容十分简洁,其中 hasownKeys 实质也属于 读取操作,因而须要通过 track() 进行依赖收集,而 deleteProperty 相当于批改操作,因而须要 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)
  // 指标对象上存在对应的 key,并且能胜利删除,才会触发依赖更新
  if (result && hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

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

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

数组类型捕捉器 —— arrayInstrumentations

数组类型 对象类型 的大部分操作是能够共用的,比方 obj.namearr[index] 等,但数组类型的操作还是会比对象类型更丰盛一些,而这些就须要非凡解决。

源码地位:packages\reactivity\src\collectionHandlers.ts

解决数组索引 indexlength

数组的 indexlength 是会相互影响的,比方存在数组 const arr = [1]

  • arr[1] = 2 的操作会隐式批改 length 的属性值
  • arr.length = 0 的操作会导致原索引位的值产生变更

为了可能正当触发和 length 相干副作用函数的执行,在 set() 捕捉器中会判断以后操作的类型:

  • Number(key) < target.length 证实是批改操作,对应 TriggerOpTypes.SET 类型,即以后操作不会扭转 length 的值,不须要 触发和 length 相干副作用函数的执行
  • Number(key) >= target.length 证实是新增操作,TriggerOpTypes.ADD 类型,即以后操作会扭转 length 的值,须要 触发和 length 相干副作用函数的执行
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
  
   省略其余代码
   
    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
    
    if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
解决数组的查找办法

数组的查找办法包含 includesindexOflastIndexOf,这些办法通常状况下是可能按预期进行工作,但还是须要对某些非凡状况进行解决:

  • 当查找的指标数据是响应式数据自身时,失去的就不是预期后果

    const obj = {}
    const proxy = reactive([obj])
    console.log(proxy.includs(proxy[0])) // false
    • 产生起因 】首先这里波及到了两次读取操作, 第一次 proxy[0] 此时会触发 get 捕捉器并为 obj 生成对应代理对象并返回, 第二次proxy.includs() 的调用,它会遍历数组的每个元素,即会触发 get 捕捉器,并又生成一个新的代理对象并返回,而这两次生成的代理对象不是同一个,因而返回 false
    • 解决方案】源码中会在 get 中设置一个名为 proxyMapWeakMap 汇合用于存储每个响应式对象,在触发 get 时优先返回 proxyMap 存在的响应式对象,这样不论触发多少次 get 都能返回雷同的响应式数据
  • 当在响应式对象中查找原始数据时,失去的就不是预期后果

    const obj = {}
    const proxy = reactive([obj])
    console.log(proxy.includs(obj)) // false
    • 产生起因 proxy.includes() 会触发 get 捕捉器并为 obj 生成对应代理对象并返回,而 includes 办法的参数传递的是 原始数据 ,相当于此时是 响应式对象 原始数据对象 进行比拟,因而对应的后果肯定是为 false
    • 解决方案 】外围就是将它们的数据类型对立,即对立都应用 原始值数据比照 响应式数据比照,因为 includes() 的办法自身并不反对对传入参数或外部响应式数据的解决,因而须要自定义以上对应的数组查找办法

      • 在 重写 / 加强 的 includesindexOflastIndexOf 等办法中,会将以后办法外部拜访到的响应式数据转换为原始数据,而后调用数组对应的原始办法进行查找,若查找后果为 true 则间接返回后果
      • 若以上操作没有查找到,则通过将以后办法传入的参数转换为原始数据,在调用数组的原始办法,此时间接将对应的后果向外进行返回

源码地位:packages\reactivity\src\baseHandlers.ts

;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 内部调用上述办法,默认其内的 this 指向的是代理数组对象,// 但实际上是须要通过原始数组中进行遍历查找
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {track(arr, TrackOpTypes.GET, i + '')
      }
      // we run the method using the original args first (which may be reactive)
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        return arr[key](...args.map(toRaw))
      } else {return res}
    }
  })
解决数组影响 length 的办法

隐式批改数组长度的原型办法包含 pushpopshiftunshiftsplice 等,在调用这些办法的同时会间接的读取数组的 length 属性,又因为这些办法具备批改数组长度的能力,即相当于 length 的设置操作,若不进行非凡解决,会导致与 length 属性相干的副作用函数被反复执行,即 栈溢出,比方:

const proxy = reactive([])

// 第一个副作用函数
effect(() => {proxy.push(1) // 读取 + 设置 操作
})

// 第二个副作用函数
effect(() => {proxy.push(2) // 读取 + 设置 操作(此时进行 trigger 时,会触发包含第一个副作用函数的内容,而后循环导致栈溢出)})

在源码中还是通过 重写 / 加强 上述对应数组办法的模式实现自定义的逻辑解决:

  • 在调用真正的数组原型办法前,会通过设置 pauseTracking() 办法来禁止 track 依赖收集
  • 在调用数组原生办法后,在通过 resetTracking() 办法复原 track 进行依赖收集
  • 实际上以上的两个办法就是通过管制 shouldTrack 变量为 truefalse,使得在 track 函数执行时是否要执行原来的依赖收集逻辑

源码地位:packages\reactivity\src\baseHandlers.ts

;(['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
    }
  })

汇合类型的捕捉器 — mutableCollectionHandlers

汇合类型 包含 MapWeakMapSetWeakSet 等,而对 汇合类型 代理模式 对象类型 须要有所不同,因为 汇合类型 对象类型 的操作方法是不同的,比方:

Map 类型 的原型 属性 办法 如下, 详情可见

  • size
  • clear()
  • delete(key)
  • has(key)
  • get(key)
  • set(key)
  • keys()
  • values()
  • entries()
  • forEach(cb)

Set 类型 的原型 属性 办法 如下, 详情可见

  • size
  • add(value)
  • clear()
  • delete(value)
  • has(value)
  • keys()
  • values()
  • entries()
  • forEach(cb)

    源码地位:packages\reactivity\src\collectionHandlers.ts

解决 代理对象 无法访问 汇合类型 对应的 属性 办法

代理汇合类型的第一个问题,就是代理对象没法获取到汇合类型的属性和办法,比方:

从报错信息能够看出 size 属性是一个拜访器属性,所以它被作为办法调用了,而次要谬误起因就是在这个拜访器中的 this 指向的是 代理对象 ,在源码中就是通过为这些特定的 属性 办法 定义对应的 keymutableInstrumentations 对象,并且在其对应的 属性 办法 中将 this 指向为 原对象.

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  if (key !== rawKey) {!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

function size(target: IterableCollections, isReadonly = false) {target = (target as any)[ReactiveFlags.RAW]
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}

省略其余代码
解决汇合类型的响应式

汇合建设响应式外围还是 tracktrigger,转而思考的问题就变成,什么时候须要 track、什么时候须要 trigger:

  • track 机会:get()、get size()、has()、forEach()
  • trigger 机会:add()、set()、delete()、clear()

这里波及一些优化的内容,比方:

  • add() 中通过 has() 判断以后增加的元素是否曾经存在于 Set 汇合中时,若已存在就不须要进行 trigger() 操作,因为 Set 汇合自身的一个个性就是 去重
  • delete() 中通过 has() 判断以后删除的元素或属性是否存在,若不存在就不须要进行 trigger() 操作,因为此时的删除操作是 有效的
function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {get(this: MapTypes, key: unknown) {// track
      return get(this, key)
    },
    get size() {// track
      return size(this as unknown as IterableCollections)
    },
    has,// track
    add,// trigger
    set,// trigger
    delete: deleteEntry,// trigger
    clear,// trigger
    forEach: createForEach(false, false) // track
  }
  省略其余代码
}
防止净化原始数据

通过重写汇合类型的办法并手动指定其中的 this 指向为 原始对象 的形式,解决 代理对象 无法访问 汇合类型 对应的 属性 办法 的问题,但这样的实现形式也带来了另一个问题: 原始数据被净化

简略来说,咱们只心愿 代理对象(响应式对象 才具备 依赖收集 (track) 依赖更新 (trigger) 的能力,而通过 原始数据 进行的操作不应该具备响应式的能力。

如果只是单纯的把所有操作间接作用到 原始对象 上就不能保障这个后果,比方:

 // 原数数据 originalData1
  const originalData1 = new Map({});
  // 代理对象 proxyData1
  const proxyData1 = reactive(originalData1);

  // 另一个代理对象 proxyData2
  const proxyData2 = reactive(new Map({}));

  // 将 proxyData2 做为 proxyData1 一个键值
  //【留神】此时的 set() 通过重写,其外部 this 曾经指向 原始对象(originalData1),等价于 原始对象 originalData1 上存储了一个 响应式对象 proxyData2
  proxyData1.set("proxyData2", proxyData2);

  // 若不做额定解决,如下基于 原始数据的操作 就会触发 track 和 trigger
  originalData1.get("proxyData2").set("name", "zs");

在源码中的解决方案也是很简略,间接通过 value = toRaw(value) 获取以后设置值对应的 原始数据 ,这样旧能够防止 响应式数据对原始数据的净化

解决 forEach 回调参数

首先 Map.prototype.forEach(callbackFn [, thisArg]) 其中 callbackFn 回调函数会接管三个参数:

  • 以后的 value
  • 以后的 key
  • 正在被遍历的 Map 对象(原始对象)

遍历操作 等价于 读取操作 ,在解决 一般对象 get() 捕捉器中有一个解决,如果以后拜访的属性值是 对象类型 那么就会向外返回其对应的 代理对象 ,目标是实现 惰性响应 深层响应 ,这个解决也同样实用于 汇合类型

因而,在源码中通过 callback.call(thisArg, wrap(value), wrap(key), observed) 的形式将 Map 类型的 进行响应式解决,以及进行 track 操作,因为 Map 类型关注的就是

function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // important: make sure the callback is
      // 1. invoked with the reactive map as `this` and 3rd arg
      // 2. the value received should be a corresponding reactive/readonly.
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
解决迭代器

汇合类型的迭代器办法:

  • entries()
  • keys()
  • values()

MapSet 都实现了 可迭代协定 (即 Symbol.iterator 办法,而 迭代器协定 是指 一个对象实现了 next 办法),因而它们还能够通过 for...of 的形式进行遍历。

依据对 forEach 的解决,不难晓得波及遍历的办法,究竟还是得将其对应的遍历的 键、值 进行响应式包裹的解决,以及进行 track 操作,而本来的的迭代器办法没方法实现,因而须要外部自定义迭代器协定。

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
    省略其余代码
  })

这一部分的源码波及的内容比拟多,以上只是简略的总结一下,更具体的内容可查看对应的源码内容。

ref 函数 — 原始值的响应式

原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null 等类型的值,咱们晓得用 Object.defineProperty 必定是不反对,因为它拦挡的就是对象属性的操作,都说 ProxyObject.defineProperty 强,那么它能不能间接反对呢?

间接反对是必定不能的,别忘了 Proxy 代理的指标也还是对象类型呀,它的强是在本人的所属畛域,跨畛域也是遭不住的。

因而在 Vue3ref 函数中对原始值的解决形式是通过为 原始值类型 提供一个通过 new RefImpl(rawValue, shallow) 实例化失去的 包裹对象 ,说白了还是将原始值类型变成对象类型,但 ref 函数的参数并 不限度数据类型

  • 原始值类型 ref 函数中会为原始值类型数据创立 RefImpl 实例对象(必须通过 .value 的形式拜访数据),并且实现自定义的 get、set 用于别离进行 依赖收集 依赖更新 ,留神的是这里并不会通过 Proxy 为原始值类型创立代理对象,精确的说在 RefImpl 外部自定义实现的 get、set 就实现了对原始值类型的拦挡操作, 因为原始值类型不须要向对象类型设置那么多的捕捉器
  • 对象类型 ref 函数中除了为 对象类型 数据创立 RefImpl 实例对象之外,还会通过 reactive 函数将其转换为响应式数据,其实次要还是为了反对相似如下的操作

    const refProxy = ref({name: 'zs'})
    refProxy.value.name = 'ls'
  • 依赖容器 dep,在 ref 类型中依赖存储的地位就是每个 ref 实例对象上的 dep 属性,它实质就是一个 Set 实例,触发 get 时往 dep 中增加副作用函数(依赖),触发 set 时从 dep 中顺次取出副作用函数执行

源码地位:packages\reactivity\src\ref.ts

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

function createRef(rawValue: unknown, shallow: boolean) {if (isRef(rawValue)) {return rawValue}
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 将依赖收集到 dep 中,实际上就是一个 Set 类型
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    // 获取原始数据
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)

    // 通过 Object.is(value, oldValue) 判断新旧值是否统一,若不统一才须要进行更新
    if (hasChanged(newVal, this._rawValue)) {
      // 保留原始值
      this._rawValue = newVal
      // 更新为新的 value 值
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 依赖更新,从 dep 中取出对应的 effect 函数顺次遍历执行
      triggerRefValue(this, newVal)
    }
  }
}

// 若以后 value 是 对象类型,才会通过 reactive 转换为响应式数据
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

Vue3 如何进行依赖收集?

Vue2 中依赖的收集形式是通过 DepWatcher观察者模式 来实现的,是不是还能想起首次理解 DepWatcher 之间的这种 剪一直理还乱 的关系时的情绪 ……

对于 设计模式 局部感兴趣可查看 常见 JavaScript 设计模式 — 原来这么简略 一文,外面次要围绕着 Vue 中对应的设计模式来进行介绍,置信会有肯定的帮忙

依赖收集 其实说的就是 track 函数须要解决的内容:

  • 申明 targetMap 作为一个容器,用于保留和以后响应式对象相干的依赖内容,自身是一个 WeakMap 类型

    • 抉择 WeakMap 类型作为容器,是因为 WeakMap(对象类型)的援用是 弱类型 的,一旦内部没有对该 (对象类型)放弃援用时,WeakMap 就会主动将其删除,即 可能保障该对象可能失常被垃圾回收
    • Map 类型对 的援用则是 强援用,即使内部没有对该对象放弃援用,但至多还存在 Map 自身对该对象的援用关系,因而会导致该对象不能及时的被垃圾回收
  • 将对应的 响应式数据对象 作为 targetMap,存储和以后响应式数据对象相干的依赖关系 depsMap(属于 Map 实例),即 depsMap 存储的就是和以后响应式对象的每一个 key 对应的具体依赖
  • deps(属于 Set 实例)作为 depsMap 每个 key 对应的依赖汇合,因为每个响应式数据可能在多个副作用函数中被应用,并且 Set 类型用于主动去重的能力

可视化构造如下:

源码地位:packages\reactivity\src\effect.ts

const targetMap = new WeakMap<any, KeyToDepMap>()

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 以后应该进行依赖收集 且 有对应的副作用函数时,才会进行依赖收集
  if (shouldTrack && activeEffect) {
    // 从容器中取出【对应响应式数据对象】的依赖关系
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 若不存在,则进行初始化
      targetMap.set(target, (depsMap = new Map()))
    }

    // 获取和对【应响应式数据对象 key】相匹配的依赖
    let dep = depsMap.get(key)
    if (!dep) {
      // 若不存在,则进行初始化 dep 为 Set 实例
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? {effect: activeEffect, target, type, key}
      : undefined

    // 往 dep 汇合中增加 effect 依赖
    trackEffects(dep, eventInfo)
  }
}

export const createDep = (effects?: ReactiveEffect[]): Dep => {const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

最初

以上就是针对 Vue3 中对不同数据类型的解决的内容,无论是 Vue2 还是 Vue3 响应式的外围都是 数据劫持 / 代理、依赖收集、依赖更新,只不过因为实现数据劫持形式的差别从而导致具体实现的差别,在 Vue3 中值得注意的是:

  • 一般对象类型 能够间接配合 Proxy 提供的捕捉器实现响应式
  • 数组类型 也能够间接复用大部分和 一般对象类型 的捕捉器,但其对应的查找办法和隐式批改 length 的办法依然须要被 重写 / 加强
  • 为了反对 汇合类型 的响应式,也对其对应的办法进行了 重写 / 加强
  • 原始值数据类型 次要通过 ref 函数来进行响应式解决,不过内容不会对 原始值类型 应用 reactive(或 Proxy) 函数来解决,而是在外部自定义 get value(){}set value(){} 的形式实现响应式,毕竟原始值类型的操作无非就是 读取 设置 ,外围还是将 原始值类型 转变为了 一般对象类型

    • ref 函数可实现原始值类型转换为 响应式数据,但 ref 接管的值类型并没只限定为原始值类型,若接管到的是援用类型,还是会将其通过 reactive 函数的形式转换为响应式数据

肝了近 1W 字的内容,也是目前写得字数最多的文章,人有点麻了 … 哈哈,心愿对大家有所帮忙!!!

退出移动版