Vue reactive 源码解析

lxf2023-12-16 16:20:01

Vue reactive 源码解析

写在最前面

reactivity模块中,之所以最后才写reactive是因为它的原理虽然不复杂,大家都知道是用的proxy拦截操作,但是实际上代码逻辑比较复杂,对于不同的类型拦截操作不同,笔者花了一点时间读代码和调试,虽然如此,由于能力所限,肯定也不能写全,还望见谅。

reactive 原理综述

reactive通过proxy对象类型(包括对象、数组、集合类型)进行代理,拦截getsetdeletePropertyhasownKeys操作,在拦截过程中进行依赖的收集和触发,下面分别对原理做简要的介绍

  • 对于对象

    • reactive会拦截对于每个属性的操作。一般来说,当读取对象的某个key的值的时候,会通过Reflect.get获得该key对应的值res,并调用track函数收集依赖,如果res也是对象,则会返回reactive(res)(为了实现对于对象的深层次的响应式代理),否则返回res。对于对象属性值的设置会被set拦截,并通过trigger函数触发依赖
    • 其他操作如in操作符会被has拦截,delete会被deleteProperty拦截,for...inObject.keys()会被ownKeys拦截,等等
  • 对于数组类型

    • 当操作为类似arr[0] = 1时,由于数组为键为"0"、"1"、"2"的对象,所以对于这种操作,响应式的原理和对象相同

    • 对于for...of循环,实际上会通过[Symbol.iterator]()方法获得数组的默认迭代器对象iterator,并获得iterator.next().value,这一操作会被proxyget操作拦截。集合类型对象同理

    • 当调用数组的push、pop、shift等原生方法时,会在数组的get中被拦截,并调用Vue 3重载的数组方法。重载方法会触发依赖的更新,并调用原生方法进行对应的操作,这些重载方法写在arrayInstrumentations

    • 当改变数组的"length"属性时,使用到arr.length的副作用肯定会被触发;对于仅使用到arr[i]的副作用,需要根据ilength的大小关系进行判断:只有当i大于等于新的length时,副作用会被重新触发

      const arr = reactive([1,2,3,4,5,6,7,8])
      
      // 不会被重新触发
      watchEffect(() => {
        console.log(arr[0])
      })
      
      // 会被重新触发
      watchEffect(() => {
        console.log(arr[7])
      })
      
      arr.length = 3 // 更改 length 属性
      
  • 对于集合类型MapSetWeakMapWeakSet),和数组类型的方法拦截相同,Vue也同样拦截了对于原生方法(如set、get、delete、has、forEach等方法)的调用,并调用了重载方法,从而触发依赖的更新,这些重载方法的逻辑写在createInstrumentations函数中

正文

正文部分介绍reactive的核心逻辑,shallowReactive、readonlyAPI的核心逻辑类似。

reactive函数中,会通过createReactiveObject创建源对象的响应式对象,创建过程的核心部分就是通过代理创建源对象的响应式代理

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

对于集合类型COLLECTION),会使用collectionHandlers进行代理;对于对象和数组类型会使用baseHandlers进行代理。

对象和数组类型

对于reactive API,对象和数组类型使用mutableHandlers进行代理,对get、set、deleteProperty、has、ownKeys操作进行了拦截

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

get 操作

get操作的主要逻辑写在了createGetter函数返回的get函数中,解释写在注释中了。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 访问 target 的一些 vue 自己添加的属性,例如__v_isReactive、__v_isReadonly等
    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) {
      // 如果 target 是数组,并且访问的是 push 等原生方法,那么就返回 vue 重载的方法
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      // 如果访问的是对象的 hasOwnProperty 方法,则返回 vue 重载的 hasOwnProperty 方法
      // 上面重载的是数组方法,这里是重载了对象的方法
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }

    const res = Reflect.get(target, key, receiver) // 调用 Reflect.get 获得 key 对应的值 res

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
		
    // 只读对象因为无法更改,不具有响应性,所以不需要收集依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key) // track 函数收集 target 的 key 依赖
    }

    if (shallow) {
      return res // shallowReactive 不需要具有深层次响应性,直接返回 res 即可
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      // ref 解包,返回 res.value
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      
      // 对于 res 也为对象的情况返回 reactive(res) 从而实现深层次响应性
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

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 (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      /*
      对应情况:
      const obj = readonly(ref(0))
      const reactiveObj = reactive({ obj })
      reactiveObj.obj = 1 // false
      */
      return false
    }
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        // 获得代理的原始对象
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 解包的逻辑
        /*
        对应情况:
        const obj = ref(0)
        const reactiveObj = reactive({ obj })
        reactiveObj.obj = 1 // 相当于更改了 obj.value
        */
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    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
  }
}

deleteProperty & has & ownKeys

function deleteProperty(target: object, key: string | symbol): boolean {
  // 删除属性,如果删除成功并且 key 是 target 的自有属性,则触发依赖更新
  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
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  // 如果 key 不是内置 symbol 类型或 vue 内部使用的 symbol 类型则收集依赖
  // 原因见下面 chatgpt 的回答
  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)
}

Vue reactive 源码解析

集合类型

这部分的主要任务就是当调用集合类型的原生方法时,改为调用vue重载的方法

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    return Reflect.get(
      // 如果访问的属性在 instrumentations 上存在,即访问的是 vue 重载的方法
      // 则访问 instrumentations 上实现的重载方法
      // 否则返回 target 上的对应属性
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

总结

reactive的主要原理是使用proxy创造原始对象的代理,拦截对于源对象的操作,在该过程中执行依赖的收集和触发,如果调用的是对象的原生方法,则执行vue重载的方法,从而能够进行在方法中进行依赖的收集和触发。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!