关于前端:Vue3-源码解析六响应式原理与-reactive

10次阅读

共计 9931 个字符,预计需要花费 25 分钟才能阅读完成。

明天这篇文章是笔者会带着大家一起深刻分析 Vue3 的响应式原理实现,以及在响应式根底 API 中的 reactive 是如何实现的。对于 Vue 框架来说,其非侵入的响应式零碎是最独特的个性之一了,所以不管任何一个版本的 Vue,在相熟其根底用法后,响应式原理都是笔者最想优先理解的局部,也是浏览源码时必细细钻研的局部。毕竟知己知彼百战不殆,当你应用 Vue 时,把握了响应式原理肯定会让你的 coding 过程更加熟能生巧的。

Vue2 的响应式原理

在开始介绍 Vue3 的响应式原理前,咱们先一起回顾一下 Vue2 的响应式原理。

当咱们把一个一般选项传入 Vue 实例的 data 选项中,Vue 将遍历此对象所有的 property,并应用 Object.defineProperty 把这些 property 全副转为 getter/setter。而 Vue2 在解决数组时,也会通过原型链劫持会扭转数组内元素的办法,并在原型链察看新增的元素,以及派发更新告诉。

这里放上一张 Vue2 文档中介绍响应式的图片。对于文档中有的形容笔者就不再赘述,而从 Vue2 的源码角度来对照图片说一说。在 Vue2 的源码中的 src/core 门路下有一个 observer 模块,它就是 Vue2 中解决响应式的中央了。在这个模块下 observer 负责将对象、数组转换成响应式的,即图中的紫色局部,解决 Data 的 getter 及 setter。当 data 中的选项被拜访时,会触发 getter,此时 observer 目录下的 wather.js 模块就会开始工作,它的工作就是收集依赖,咱们收集到的依赖是一个个 Dep 类的实例化对象。而 data 中的选项变更时,会触发 setter 的调用,而在 setter 的过程中,触发 dep 的 notify 函数,派发更新事件,由此实现数据的响应监听。

Vue3 的响应式变动

在简略回顾了 Vue2 的响应式原理后,咱们会有一个纳闷,Vue3 的响应式原理与 Vue2 相比有什么不同呢?

在 Vue3 中响应式零碎最大的区别就是,数据模型是被代理的 JavaScript 对象了。不论是咱们在组件的 data 选项中返回一个一般的 JavaScript 对象,还是应用 composition api 创立一个 reactive 对象,Vue3 都会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。

Proxy 对象用于创立一个对象的代理,从而实现基本操作的拦挡和自定义(如属性查找、赋值等)。

其根底语法相似于:

const p = new Proxy(target, handler)

Proxy 相比拟于 Object.defineProperty 到底有什么劣势呢?这个问题让咱们先从 Object.defineProperty 的弊病说起。

从 Object 的角度来说,因为 Object.defineProperty 是对指定的 key 生成 getter/setter 以进行变动追踪,那么如果这个 key 一开始不存在咱们定义的对象上,响应式零碎就无能为力了,所以在 Vue2 中无奈检测对象的 property 的增加或移除。而对于这个缺点,Vue2 提供了 vm.$set 和全局的 Vue.set API 让咱们可能向对象增加响应式的 property。

从数组的角度来说,当咱们间接利用索引设置一个数组项时,或者当咱们批改数组长度时,Vue2 的响应式零碎都不能监听到变动,解决的办法也如上,应用下面提及的 2 个 api。

而这些问题在 ES6 的新个性 Proxy 背后统统都是不存在的,Proxy 对象可能利用 handler 陷阱在 get、set 时捕捉到任何变动,也能监听对数组索引的改变以及 数组 length 的改变。

而依赖收集和派发更新的形式在 Vue3 中也变得不同,在这里我先疾速的整体形容一下:在 Vue3 中,通过 track 的处理器函数来收集依赖,通过 trigger 的处理器函数来派发更新,每个依赖的应用都会被包裹到一个副作用(effect)函数中,而派发更新后就会执行副作用函数,这样依赖处的值就被更新了。

响应式根底 reactive 的实现

既然这是一个源码剖析的文章,咱们还是从源码的角度来剖析响应式到底是如何实现的。所以笔者会先剖析响应式根底的 API —— reactive,置信通过解说 reactive 的实现,大家会对 Proxy 有更粗浅的意识。

reactive

二话不说,间接看源码。上面是 reactive API 的函数,函数的参数承受一个对象,通过 createReactiveObject 函数解决后,间接返回一个 proxy 对象。

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 如果试图去察看一个只读的代理对象,会间接返回只读版本
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {return target}
  // 创立一个代理对象并返回
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

在第三行能看到通过判断 target 中是否有 ReactiveFlags 中的 IS_READONLY key 确定对象是否为只读对象。ReactiveFlags 枚举会在源码中一直的与咱们见面,所以有必要提前介绍一下 ReactiveFlags:

export const enum ReactiveFlags {
  SKIP = '__v_skip', // 是否跳过响应式 返回原始对象
  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
  IS_READONLY = '__v_isReadonly', // 标记一个只读对象
  RAW = '__v_raw' // 标记获取原始值
}

在 ReactiveFlags 枚举中有 4 个枚举值,这四个枚举值的含意都在正文里。对于 ReactiveFlags 的应用是代理对象对 handler 中的 trap 陷阱十分好的利用,对象中并不存在这些 key,而通过 get 拜访这些 key 时,返回值都是通过 get 陷阱的函数内解决的。介绍完 ReactiveFlags 后咱们持续往下看。

createReactiveObject

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

先看 createReactiveObject 函数的签名,该函数承受 5 个参数:

  • target:指标对象,想要生成响应式的原始对象。
  • isReadonly:生成的代理对象是否只读。
  • baseHandlers:生成代理对象的 handler 参数。当 target 类型是 Array 或 Object 时应用该 handler。
  • collectionHandlers:当 target 类型是 Map、Set、WeakMap、WeakSet 时应用该 handler。
  • proxyMap:存储生成代理对象后的 Map 对象。

这里须要留神的是 baseHandlers 和 collectionHandlers 的区别,这两个参数会依据 target 的类型进行判断,最终抉择将哪个参数传入 Proxy 的构造函数,当做 handler 参数应用。

接着咱们开始看 createReactiveObject 的逻辑局部:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果指标不是对象,间接返回原始值
  if (!isObject(target)) {return target}
  // 如果指标曾经是一个代理,间接返回
  // 除非对一个响应式对象执行 readonly
  if (target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {return target}
  // 指标曾经存在对应的代理对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {return existingProxy}
  // 只有白名单里的类型能力被创立响应式对象
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {return target}
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

在该函数的逻辑局部,能够看到根底数据类型并不会被转换成代理对象,而是间接返回原始值。

并且会将曾经生成的代理对象缓存进传入的 proxyMap,当这个代理对象已存在时不会反复生成,会间接返回已有对象。

也会通过 TargetType 来判断 target 指标对象的类型,Vue3 仅会对 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其余对象会被标记为 INVALID,并返回原始值。

当指标对象通过类型校验后,会通过 new Proxy() 生成一个代理对象 proxy,handler 参数的传入也是与 targetType 相干,并最终返回已生成的 proxy 对象。

所以回顾 reactive api,咱们可能会失去一个代理对象,也可能只是取得传入的 target 指标对象的原始值。

Handlers 的组成

在 @vue/reactive 库中有 baseHandlers 和 collectionHandlers 两个模块,别离生成 Proxy 代理的 handlers 中的 trap 陷阱。

例如在下面生成 reactive 的 api 中 baseHandlers 的参数传入了一个 mutableHandlers 对象,这个对象是这样的:

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

通过变量名咱们能晓得 mutableHandlers 中存在 5 个 trap 陷阱。而在 baseHandlers 中,get 和 set 都是通过工厂函数生成的,以便于适配除 reactive 外的其余 api,例如 readonly、shallowReactive、shallowReadonly 等。

baseHandlers 是解决 Array、Object 的数据类型的,这也是咱们绝大部分工夫应用 Vue3 时应用的类型,所以笔者接下来着重的讲一下 baseHandlers 中的 get 和 set 陷阱。

get 陷阱

上一段提到 get 是由一个工厂函数生成的,先来看一下 get 陷阱的品种。

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

get 陷阱有 4 个类型,别离对应不同的响应式 API,从名称中就能够晓得对应的 API 名称,十分高深莫测。而所有的 get 都是由 createGetter 函数生成的。所以接下来咱们着重看一下 createGetter 的逻辑。

还是老规矩,先从函数签名看起。

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

createGetter 有 isReadonly 和 shallow 两个参数,让应用 get 陷阱的 api 按需应用。而函数的外部返回了一个 get 函数,应用高阶函数的形式返回将会传入 handlers 中 get 参数的函数。

接着看 createGetter 的逻辑:

// 如果 get 拜访的 key 是 '__v_isReactive',返回 createGetter 的 isReadonly 参数取反后果
if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {
  // 如果 get 拜访的 key 是 '__v_isReadonly',返回 createGetter 的 isReadonly 参数
  return isReadonly
} else if (
  // 如果 get 拜访的 key 是 '__v_raw',并且 receiver 与原始标识相等,则返回原始值
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
    ).get(target)
) {return target}

从这段 createGetter 逻辑中,笔者专门介绍过的 ReactiveFlags 枚举在这就获得了妙用。其实指标对象中并没有这些 key,然而在 get 中 Vue3 就对这些 key 做了非凡解决,当咱们在对象上拜访这几个非凡的枚举值时,就会返回特定意义的后果。而能够关注一下 ReactiveFlags.IS_REACTIVE 这个 key 的判断形式,为什么是只读标识的取反呢?因为当一个对象的拜访能触发这个 get 陷阱时,阐明这个对象必然曾经是一个 Proxy 对象了,所以只有不是只读的,那么就能够认为是响应式对象了。

接着看 get 的后续逻辑。

持续判断 target 是否是一个数组,如果代理对象不是只读的,并且 target 是一个数组,并且拜访的 key 在数组须要非凡解决的办法里,就会间接调用非凡解决的数组函数执行后果,并返回。

arrayInstrumentations 是一个对象,对象内保留了若干个被非凡解决的数组办法,并以键值对的模式存储。

咱们之前说过 Vue2 以原型链的形式劫持了数组,而在这里也有相似地作用,而数组的局部咱们筹备放在后续的文章中再介绍,上面是须要非凡解决的数组。

  • 对索引敏感的数组办法

    • includes、indexOf、lastIndexOf
  • 会扭转本身长度的数组办法,须要防止 length 被依赖收集,因为这样可能会造成循环援用

    • push、pop、shift、unshift、splice
// 判断 taeget 是否是数组
const targetIsArray = isArray(target)
// 如果不是只读对象,并且指标对象是个数组,拜访的 key 又在数组须要劫持的办法里,间接调用批改后的数组办法执行
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}

// 获取 Reflect 执行的 get 默认后果
const res = Reflect.get(target, key, receiver)

// 如果是 key 是 Symbol,并且 key 是 Symbol 对象中的 Symbol 类型的 key
// 或者 key 是不须要追踪的 key: __proto__,__v_isRef,__isVue
// 间接返回 get 后果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}

// 不是只读对象,执行 track 收集依赖
if (!isReadonly) {track(target, TrackOpTypes.GET, key)
}

// 如果是 shallow 浅层响应式,间接返回 get 后果
if (shallow) {return res}

if (isRef(res)) {
  // 如果是 ref,则返回解包后的值 - 当 target 是数组,key 是 int 类型时,不须要解包
  const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  return shouldUnwrap ? res.value : res
}

if (isObject(res)) {
  // 将返回的值也转换成代理,咱们在这里做 isObject 的查看以防止有效值正告。// 也须要在这里惰性拜访只读和星影视对象,以防止循环依赖。return isReadonly ? readonly(res) : reactive(res)
}

// 不是 object 类型则间接返回 get 后果
return res

在解决完数组后,咱们对 target 执行 Reflect.get 办法,取得默认行为的 get 返回值。

之后判断 以后 key 是否是 Symbol,或者是否是不须要追踪的 key,如果是的话间接返回 get 的后果 res。

上面👇几个 key 是不须要被依赖收集或者返回响应式后果的。

  • __proto__
  • _v_isRef
  • __isVue

接着判断以后代理对象是否是只读对象,如果不是只读的话,则运行笔者上文提及的 tarck 处理器函数收集依赖。

如果是 shallow 的浅层响应式,则不须要将外部的属性转换成代理,间接返回 res。

如果 res 是一个 Ref 类型的对象,就会主动解包返回,这里就能解释官网文档中提及的 ref 在 reactive 中会主动解包的个性了。而须要留神的是,当 target 是一个数组类型,并且 key 是 int 类型时,即应用索引拜访数组元素时,不会被主动解包。

如果 res 是一个对象,就会将该对象转成响应式的 Proxy 代理对象返回,再联合咱们之前剖析的缓存已生成的 proxy 对象,能够晓得这里的逻辑并不会反复生成雷同的 res,也能够了解文档中提及的当咱们拜访 reactive 对象中的 key 是一个对象时,它也会主动的转换成响应式对象,而且因为在此处生成 reactive 或者 readonly 对象是一个提早行为,不须要在第一工夫就遍历 reactive 传入的对象中的所有 key,也对性能的晋升是一个帮忙。

当 res 都不满足上述条件时,间接返回 res 后果。例如根底数据类型就会间接返回后果,而不做非凡解决。

至此,get 陷阱的逻辑全副完结了。

set 陷阱

与 createGetter 对应,set 也有一个 createSetter 的工厂函数,也是通过柯里化的形式返回一个 set 函数。

函数签名都大同小异,那么接下来笔者间接带大家盘逻辑。

set 的函数比拟简短,所以这次一次性把写好正文的代码放上来,先看代码再讲逻辑。

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {let oldValue = (target as any)[key]
    if (!shallow) {value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 当不是 shallow 模式时,判断旧值是否是 Ref,如果是则间接更新旧值的 value
      // 因为 ref 有本人的 setter
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {// shallow 模式不须要非凡解决,对象按原样 set}
        
    // 判断 target 中是否存在 key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect.set 获取默认行为的返回值
    const result = Reflect.set(target, key, value, receiver)
    // 如果指标是原始对象原型链上的属性,则不会触发 trigger 派发更新
    if (target === toRaw(receiver)) {
      // 应用 trigger 派发更新,依据 hadKey 区别调用事件
      if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

在 set 的过程中会首先获取新旧与旧值,当目前的代理对象不是浅层比拟时,会判断旧值是否是一个 Ref,如果旧值不是数组且是一个 ref 类型的对象,并且新值不是 ref 对象时,会间接批改旧值的 value。

看到这里可能会有疑难,为什么要更新旧值的 value?如果你应用过 ref 这个 api 就会晓得,每个 ref 对象的值都是放在 value 里的,而 ref 与 reactive 的实现是有区别的,ref 其实是一个 class 实例,它的 value 有本人的 set,所以就不会在这里持续进行 set 了。ref 的局部在后续的文章中会具体解说。

在解决完 ref 类型的值后,会申明一个变量 hadKey,判断以后要 set 的 key 是否是对象中已有的属性。

接下来调用 Reflect.set 获取默认行为的 set 返回值 result。

而后会开始派发更新的过程,在派发更新前,须要保障 target 和原始的 receiver 相等,target 不能是一个原型链上的属性。

之后开始应用 trigger 处理器函数派发更新,如果 hadKey 不存在,则是一个新增属性,通过 TriggerOpTypes.ADD 枚举来标记。这里能够看到开篇剖析 Proxy 强于 Object.defineProperty 的中央,会监测到任何一个新增的 key,让响应式零碎更弱小。

如果 key 是以后 target 上曾经存在的属性,则比拟一下新旧值,如果新旧值不一样,则代表属性被更新,通过 TriggerOpTypes.SET 来标记派发更新。

在更新派发完后,返回 set 的后果 result,至此 set 完结。

总结

在明天的文章中,笔者先带大家回顾了 Vue2 的响应式原理,又开始介绍 Vue3 的响应式原理,通过比拟 Vue2 和 Vue3 的响应式零碎的区别引出 Vue3 响应式零碎的晋升之处,尤其是其中最次要的调整将 Object.defineProperty 替换为 Proxy 代理对象。

为了让大家属性 Proxy 对响应式零碎的影响,笔者着重介绍了响应式根底 API:reactive。剖析了 reactive 的实现,以及 reactive api 返回的 proxy 代理对象应用的 handlers 陷阱。并且对陷阱中咱们最罕用的 get 和 set 的源码进行剖析,置信大家在看完本篇文章当前,对 proxy 这个 ES2015 的新个性的应用又有了新的了解。

本文只是介绍 Vue3 响应式零碎的第一篇文章,所以 track 收集依赖,trigger 派发更新的过程没有具体开展,在后续的文章中打算具体解说副作用函数 effect,以及 track 和 trigger 的过程,如果心愿能具体理解响应式零碎的源码,麻烦大家点个关注省得迷路。

最初,如果这篇文章可能帮忙到你理解 Vue3 中的响应式原理和 reactive 的实现,心愿能给本文点一个喜爱❤️。如果想持续追踪后续文章,也能够关注我的账号或 follow 我的 github,再次谢谢各位可恶的看官老爷。

正文完
 0