Vue3源码 | 深入理解响应式系统上篇-reactive

651 阅读10分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

Vue3优点

在深入阅读理解Vue3响应式系统前,我们首先要知道的是Vue3是通过 Proxy 进行数据双向绑定,而Vue2采用的是 Object.defineProperty 。那 Proxy 相对于 Object.defineProperty 特性存在的优劣性对比如下:

  1. Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。
  2. 对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。
  3. 数组新增修改,Proxy 可以监听到,Object.defineProperty 不能。
  4. 若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy 差。
  5. Proxy 不兼容IE,Object.defineProperty 不兼容IE8及以下
  6. Proxy 使用上比 Object.defineProperty 方便多。

如果你对上面两个用法不了解,可以先阅读下这篇文章:Proxy与Object.defineProperty的用法与区别

接下来,正式阅读Vue3的响应式相关实现的代码。

PS:近期正在阅读Vue3源码,将陆续推出序列文章,感兴趣,点个关注,一起学习~

源码目录说明

这是从GitHub上下载下来的Vue3源码目录结构,画红色边框的reactivity目录是实现响应式的代码包,它可以被单独构建作为独立的包使用。

下面对reactivity目录主要文件进行说明:

  1. tests:描述的是代码包相关的测试用例。
  2. index.ts:用于导出包相关实现方法。
  3. reactive.ts:描述的是采用Proxy去代理对象,并进行劫持操作。

原理是:用Proxy创建代理对象,并在Proxy代理对象执行get陷阱函数读值的时候进行 track 操作,执 行set陷阱函数写值的时候进行 trigger 操作。注意这里是只针对对象的代理,不能对基本数据类型。

  1. refs.ts:主要描述的是如何解决基本数据类型代理的问题。

原理是:利用对象本身的 get | set 函数,并在 get 函数进行 track 操作,set 函数进行trigger操作。

  1. computed.ts:描述的是计算属性的实现。实际是带有 lazy 属性的 effect
  2. effect.ts:描述如何跟踪属性的变化并执行回调函数。
  3. baseHandlers.ts:对Object、Array数据类型进行代理指定的自定义拦截行为。
  4. collectionHandlers.ts:对Set, Map, WeakMap, WeakSet数据类型进行代理指定的自定义拦截行为。

使用案例

这里来看两种场景,一种直接使用vue3库,另一种使用reactivity目录单独构建的包。

场景一:Vue3库

// 直接使用vue3库
import { reactive, effect } from 'vue3.js'

const obj = reactive({ x: 1 })

effect(() => {
  patch()
})

setTimeout(() => {
  obj.x = 2
}, 1000)

function patch() {
  document.body.innerText = obj.x
}

很显然,1s后,页面显示内容变为2。说明reactive对obj对象的setter方法进行了劫持,当赋新值的时候触发了effect函数。

场景二:reactivity包

构建reactivity库,可以下载vue-next的vue3源码,然后在根目录执行如下命令,这里我采用的是yarn。

// 安装依赖
yarn install 
// 执行此命令开启构建,构建完成,会在reactivity包下生成dist目录,里面是reactivity.glob.js文件
// 该文件暴露了全局的VueReactivity对象,里面集成了包暴露的所有方法以及配置对象
// ps:为了方便查看targetMap是什么,我自个新增了该对象的暴露
yarn dev reactivity 

下面看看案例代码,实现效果与场景一是一样的。

// VueReactivity 是 reactivity目录单独构建暴露的全局变量
const { effect, track, trigger, targetMap } = VueReactivity
var obj = {
  x: 1
}

effect(() => {
  patch();
  // 这里 track 只能放在effect里面,track需要使用effect内的activeEffect
  track(obj, 'get', 'x');
  console.log(targetMap, 'targetMap')
})

setTimeout(() => {
  obj.x = 2;
  trigger(obj, 'set', 'x')
}, 1000)

function patch() {
  document.body.innerText = obj.x
}

reactive源码分析

reactive用于创建响应式对象,原理是,通过Proxy对目标对象target进行代理,进行数据操作劫持。下面看下源码,具体实现:

// 枚举了一些响应式对象的类型标识,不同类型采用不同的劫持方式
export const enum ReactiveFlags {
  // 标志SKIP,则此对像永远不会被转为代理
  SKIP = '__v_skip',  
  // 是reactive处理的代理对象
  IS_REACTIVE = '__v_isReactive',
  // 只读代理
  IS_READONLY = '__v_isReadonly',
  // 原始对象
  RAW = '__v_raw',
  REACTIVE = '__v_reactive',
  READONLY = '__v_readonly'
}
// 对外暴露的创建响应式对象的API
export function reactive(target: object) {
  // 如果是只读代理,则直接返回
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 创建响应式对象
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
// 只读代理,这里不过多解读
export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}
/*
* 返回对象是否可以观察:
* 1. 标志了SKIP状态的对象,用于不会被转换为代理,对应API是,markRaw
* 2. 对象类型属于其中一种:Object,Array,Map,Set,WeakMap,WeakSet
* 3. 对象非冻结,冻结的对象无法被修改的
*/
const canObserve = (value: Target): boolean => {
  return (
    !value[ReactiveFlags.SKIP] &&
    isObservableType(toRawType(value)) &&
    !Object.isFrozen(value)
  )
}

// 这里会根据不同的参数创建不同类型的代理
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 非对象不能返回,不能进行代理劫持,直接返回
  if (!isObject(target)) {
    return target
  }

  // 已经是代理,直接返回,不能重复劫持
  if (
    target[ReactiveFlags.RAW] && 
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 已经具有相应的代理[reactive/readonly],则返回
  const reactiveFlag = isReadonly
    ? ReactiveFlags.READONLY
    : ReactiveFlags.REACTIVE
  if (hasOwn(target, reactiveFlag)) {
    return target[reactiveFlag]
  }

  // 判断目标是否可进行代理劫持
  if (!canObserve(target)) {
    return target
  }
  // 目标对象设置代理
  const observed = new Proxy(
    target,
    // 如果是Set, Map, WeakMap, WeakSet集合数据类型,走collectionHandlers
    // 基础数据类型Object/Array 走 baseHandlers
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  def(target, reactiveFlag, observed)
  // 返回代理对象
  return observed
}

到此处的代码,主要讲述的是通过reactive创建响应式对象,对入参的必要满足条件,总结下有如下几点:

  1. 非对象不能进行代理劫持。
  2. 目标已经被劫持,则返回,不能重复劫持。
  3. 只读劫持。
  4. 对象__v_skip属性非真,且数据类型属于Object,Array,Map,Set,WeakMap,WeakSet其中一种,并且对象非冻结状态,因为对象被冻结后不能被修改。
  5. 根据不同数据类型选择不同的劫持方式。

集合类型Set, Map, WeakMap, WeakSet,collectionHandlers

基础数据类型Object,Array baseHandlers

能够传递一个正确的对象,下面看看如何通过代理对对象进行数据劫持操作,才能达到响应式的效果。这里以 baseHandlers 为例。

  1. 首先 baseHandlers 是对接口 ProxyHandler 的实现,先看看接口都定义了什么。

interface ProxyHandler<T extends object> {
    getPrototypeOf? (target: T): object | null;
    setPrototypeOf? (target: T, v: any): boolean;
    isExtensible? (target: T): boolean;
    preventExtensions? (target: T): boolean;
    getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
    has? (target: T, p: PropertyKey): boolean;
    get? (target: T, p: PropertyKey, receiver: any): any;
    set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
    deleteProperty? (target: T, p: PropertyKey): boolean;
    defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
    ownKeys? (target: T): PropertyKey[];
    apply? (target: T, thisArg: any, argArray?: any): any;
    construct? (target: T, argArray: any, newTarget?: any): object;
}

其实这里包含的是Proxy基本所有的陷阱函数。 Proxy可以拦截JavaScript引擎内部目标的底层对象操作,这些操作被拦截后会触发响应特定操作的陷阱函数。

  1. 看看mutableHandlers具体实现,挑选基本的get/和set理解
const set = /*#__PURE__*/ createSetter()
const get = /*#__PURE__*/ createGetter()

// 劫持的处理程序
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

// 主要用于追踪收集数据变化,并放入到targetMap
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 标志状态相关处理,并没有进行收集
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? (target as any)[ReactiveFlags.READONLY]
          : (target as any)[ReactiveFlags.REACTIVE])
    ) {
      return target
    }
		
    // 数组相关处理
    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 通过反射获取对象的默认行为(属于语言内部的方法),不管Proxy怎么改都没用
    const res = Reflect.get(target, key, receiver)
		
    // key是Symbol类型,原型链,或者是Ref引用类型标志,则返回,不追踪
    if (
      isSymbol(key)
        ? builtInSymbols.has(key)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
   // 追踪属性变化,这里会放入到targetMap,在effect文件里面声明的存储追踪数据的weakMap变化
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 浅层代理(如,shallowReactive),不做递归,并且ref引用没有做拆包,因为直接return。
    if (shallow) {
      return res
    }
		
    // 引用对象,如果是数组,则返回数组,如果是对象,则返回值。
    // 非浅层代理(reactive),则做了ref的拆包 
    if (isRef(res)) {
      return targetIsArray ? res : res.value
    }
		// 如果是对象,则递归进行响应式处理
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

// 用于触发依赖,从targetMap获取存在数据,执行对应的effect
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 引用类型Ref的处理
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // 在浅层模式下,不管是否响应式,对象都按原样设置
    }

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标是原型链中的某个东西,就不要触发
    if (target === toRaw(receiver)) {
      // 根据key是否存在,去触发对应的操作
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

看了上面get|set两个代码块实现,我们可以总结出,在实现响应式过程中的结论:

  1. 在get陷阱函数中,对不同类型属性做对应处理,并进行数据的track操作(追踪收集缓存)
  2. 在set陷阱函数中,根据key类型以及是否存在做对应处理,并执行trigger操作,会触发effect方法

因为这里的 tracktrigger 方法是在effect.ts文件中,那这里就不做解决,放到下一篇再讲。

其他源码说明

1. 响应式对象

我们知道Vue3能创建多种类型的响应式对象,这些响应式对象是如何定义的呢。如下可以看出其他类型的响应式对象的定义也是使用采用方法 createReactiveObject ,只是采用了不一样的处理程序。

export function shallowReactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}

// 第二个参数,表示的是是否采用浅层shallow模式,
// shallow=true,任务属性ref都不会自动解包,具体看代码
const shallowGet = /*#__PURE__*/ createGetter(false, true)
// 第一个参数,表示的是是否采用浅层shallow模式,
const shallowSet = /*#__PURE__*/ createSetter(true)
  1. shallowReactive

创建一个响应式对象,只能跟踪自身属性的变化,不对嵌套对象进行响应式处理。与 reactive 不同,使用的任何属性 ref不会被代理自动解包。因为采用的是shallow = true的模式。

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// mutating state's own properties is reactive
state.foo++
// ...but does not convert nested objects
isReactive(state.nested) // false
state.nested.bar++ // non-reactive
  1. shallowReadonly

创建一个代理,使其自身的属性为只读,但不执行嵌套对象的深度只读转换。与 readonly 不同,使用的任何属性 ref不会被代理自动解包

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// mutating state's own properties will fail
state.foo++
// ...but works on nested objects
isReadonly(state.nested) // false
state.nested.bar++ // works
2. 其他API

直接看下API源码实现。

// 检测对象是否是由reactive创建的
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 isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}
// 返回reactive或readonly代理的原始对象
export function toRaw<T>(observed: T): T {
  return (
    (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
  )
}
// 标记一个对象,使其永远不会转换为代理
export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

总结

至此大概阅读了下reactive主要核心的相关源码,这里以简易代码描述下其核心实现过程:

const reactive = (target){
  // 代理数据
  return new Proxy(target, {
    get(target, prop) {
      // 执行追踪,数据放入targetMap
      track(target, prop);
      return Reflect.get(target, prop);
    },
    set(target, prop, newVal) {
      Reflect.set(target, prop, newVal);
      // 触发effect
      trigger(target, prop);
      return true;
    }
  })
}

解读如有不正确的地方,欢迎指正。下一篇预告:解读track、trigger和effect。