前言

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

【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 字的内容,也是目前写得字数最多的文章,人有点麻了 ... 哈哈,心愿对大家有所帮忙!!!