从源码开始的vue3学习之路 - 响应性

718 阅读19分钟

从源码开始的vue3学习之路 - 响应性

作者:海东青

vue3自2020年9月18日发布以来,就造成了整个前端圈的轰动。因为这次的更新,不只是因为他是一个大的版本的迭代,更重要的是这次迭代带来的新特性

  • 新的响应性原理 —— Proxy
  • 新的语法 —— composition-api
  • 新的生态 —— vuex@nextvue-router@nextelement-plusvant@next
  • 更强大的ts类型支持

作为一个使用过vue2的前端,我相信,大部分人都知道vue2是通过Object.defineProperty这个方法,来进行双向数据绑定,而双向数据绑定,又是vue的最大亮点。

但是,如果有人问你,vue3呢?双向绑定是通过什么?稍微用点心的同学应该都可以脱口而出:Proxy

没问题,是通过Proxy,那么,是怎么做的呢?我相信很多同学会在这儿卡住,只知道是由Object.defineProperty升级为Proxy,那么是如何升级的呢?做了哪些改动呢?底层又是怎么实现的呢?

今天,我带大家从源码的角度,仔细探索一下,Vue3到底是如何进行的响应性升级。

备注:本篇文章的阅读体验极其依赖es6ts的熟悉程度。

简单介绍ProxyReflect

工欲善其事必先利其器,首先我带大家简单看看ProxyReflect

我相信,这两个特性,大部分人在不知道Vue3要使用它重构的时候,听都没听过,实不相瞒,我也是。

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

上面的介绍,是mdnProxy的描述,简单来说,就是通过Proxy创建一个拦截,在访问对象的时候需要经过这道拦截。

  • getPrototypeOf
  • setPrototypeOf
  • isExtensible
  • preventExtensions
  • getOwnPropertyDescriptor
  • defineProperty
  • has
  • get
  • set
  • deleteProperty
  • ownKeys
  • apply
  • construncor
const obj = { a: 1 }
const proxy = new Proxy(obj, {
  get() {},
  set(v) {}
})
interface ProxyHandler<T extends object> {
    apply?(target: T, thisArg: any, argArray: any[]): any;
    construct?(target: T, argArray: any[], newTarget: Function): object;
    defineProperty?(target: T, p: string | symbol, attributes: PropertyDescriptor): boolean;
    deleteProperty?(target: T, p: string | symbol): boolean;
    get?(target: T, p: string | symbol, receiver: any): any;
    getOwnPropertyDescriptor?(target: T, p: string | symbol): PropertyDescriptor | undefined;
    getPrototypeOf?(target: T): object | null;
    has?(target: T, p: string | symbol): boolean;
    isExtensible?(target: T): boolean;
    ownKeys?(target: T): ArrayLike<string | symbol>;
    preventExtensions?(target: T): boolean;
    set?(target: T, p: string | symbol, value: any, receiver: any): boolean;
    setPrototypeOf?(target: T, v: object | null): boolean;
}

interface ProxyConstructor {
    revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
    new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Reflect也提供了一系列操作符,巧的是,这些操作符与Proxy的完全一致,因此我们可以使用Reflect来进行Proxy中的操作。

declare namespace Reflect {
    function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;
    function construct(target: Function, argumentsList: ArrayLike<any>, newTarget?: Function): any;
    function defineProperty(target: object, propertyKey: PropertyKey, attributes: PropertyDescriptor): boolean;
    function deleteProperty(target: object, propertyKey: PropertyKey): boolean;
    function get(target: object, propertyKey: PropertyKey, receiver?: any): any;
    function getOwnPropertyDescriptor(target: object, propertyKey: PropertyKey): PropertyDescriptor | undefined;
    function getPrototypeOf(target: object): object | null;
    function has(target: object, propertyKey: PropertyKey): boolean;
    function isExtensible(target: object): boolean;
    function ownKeys(target: object): (string | symbol)[];
    function preventExtensions(target: object): boolean;
    function set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;
    function setPrototypeOf(target: object, proto: object | null): boolean;
}

Vue3中响应对象的外部表现及使用

我们先来看看vue3中响应对象如何使用

我们使用composition-api + ts来进行演示

<template>
  <div>
    <button @click="changeRefVal">点击这个按钮可以增加ref的值:{{refVal}}</button>
    <div />
    <button @click="changeReactiveVal">点击这个按钮可以增加reactive的值:{{reactiveVal.b}}</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, reactive } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const refVal = ref<number>(1)
    const reactiveVal = reactive({
      b: 999
    })

    const changeRefVal = () => refVal.value += 1
    const changeReactiveVal = () => reactiveVal.b += 1

    return {
      refVal,
      reactiveVal,
      changeRefVal,
      changeReactiveVal
    }
  }
});
</script>

reactivity

可以看到,随着按钮的点击,触发了我们所returnchangeRefVal以及changeReactiveVal,进行了值的修改,并且在页面上进行了修改。

跟着代码进入源码

在这个例子中,我们使用了3个由vue3暴露出来的api,分别是

  • defineComponent
  • ref
  • reactive

通过用法,我们可以大概猜到他们是干嘛用的。defineComponent用于声明组件,ref以及reactive用于声明响应式的数据。

defineComponent

首先我们来看defineComponent。按住cmd (windows中是ctrl)+ 鼠标左键,我们可以进入它的类型声明文件,runtime-core.d.ts,随之映入眼帘的是一大长串的类型声明。

export declare type DefineComponent<PropsOrPropOptions = {}, RawBindings = {}, D = {}, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = Record<string, any>, EE extends string = string, PP = PublicProps, Props = Readonly<ExtractPropTypes<PropsOrPropOptions>>, Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>> = ComponentPublicInstanceConstructor<CreateComponentPublicInstance<Props, RawBindings, D, C, M, Mixin, Extends, E, PP & Props, Defaults, true> & Props> & ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE, Defaults> & PP;

export declare function defineComponent<Props, RawBindings = object>(setup: (props: Readonly<Props>, ctx: SetupContext) => RawBindings | RenderFunction): DefineComponent<Props, RawBindings>;

export declare function defineComponent<Props = {}, RawBindings = {}, D = {}, C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, EE extends string = string>(options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>;

export declare function defineComponent<PropNames extends string, RawBindings, D, C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = Record<string, any>, EE extends string = string>(options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<Readonly<{
    [key in PropNames]?: any;
}>, RawBindings, D, C, M, Mixin, Extends, E, EE>;

是不是看的头疼。我也看的头疼,算了,直接进入vue3的源码,进入packages/runtime-core/src/apiDefineComponent.ts文件,这个文件,看着特别长,但是上面全部都是defineComponent的重载,实际上只有一个作用,那就是声明一个defineComponent方法。

我们来到第183行,在这儿有一段很简单的代码

export function defineComponent(options: unknown) {
  return isFunction(options) ? { setup: options, name: options.name } : options
}

光从js的角度来说,这个函数其实很好理解。就是声明一个名字叫做defineComponent的函数,它接收一个options参数,这个参数可以是个对象,也可以是个函数。

  • 如果是对象,那么就直接返回
  • 如果是函数,那么创建一个对象,将传进来的函数,作为其setup属性传递进去,添加一个name属性,然后 返回

总结:这个函数实际上就做了个为对象添加类型的作用,不管是对象还是函数,最终都会返回一个有namesetup的对象

关于这个函数的作用,看这个可能有点抽象,因为上面进行了好几次的重载,然后选项也有函数以及对象两种方式。

如果觉得不好理解,可以参考Vite中的defineConfig的实现

export interface ConfigEnv {
  command: 'build' | 'serve'
  mode: string
}

export type UserConfigFn = (env: ConfigEnv) => UserConfig
export type UserConfigExport = UserConfig | UserConfigFn

/**
 * Type helper to make it easier to use vite.config.ts
 * accepts a direct {@link UserConfig} object, or a function that returns it.
 * The function receives a {@link ConfigEnv} object that exposes two properties:
 * `command` (either `'build'` or `'serve'`), and `mode`.
 */
export function defineConfig(config: UserConfigExport): UserConfigExport {
  return config
}

是不是恍然大悟,瞬间觉得特别简单

ref

我们继续从这个api点进去,首先看它的类型

export declare interface Ref<T = any> {
    value: T;
    /**
     * Type differentiator only.
     * We need this to be in public d.ts but don't want it to show up in IDE
     * autocomplete, so we use a private Symbol instead.
     */
    [RefSymbol]: true;
    /* Excluded from this release type: _shallow */
}

export declare function ref<T extends object>(value: T): ToRef<T>;

export declare function ref<T>(value: T): Ref<UnwrapRef<T>>;

export declare function ref<T = any>(): Ref<T | undefined>;

declare const RefSymbol: unique symbol;

首先,从类型分析,我们可以得到几个结果

  • 我们使用ref创建响应式的值的时候,返回值是一个对象,对象里面至少存在两个属性

    • 一个是value,根据泛型可以得出,它就是我们传递进来的值
    • 还有一个Symbol属性,用来标记
  • 我们可以初始化ref,只传递泛型而不传递参数,这种用法,为vue3中的组件ref提供了类型基础

    <template>
      <div>
        <hello-world ref="hw" msg="hello world" />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref, reactive, onMounted } from 'vue'
    import HelloWorld from './components/HelloWorld.vue'
    
    export default defineComponent({
      name: 'App',
      components: {
        HelloWorld
      },
      setup() {
        const hw = ref<typeof HelloWorld>()
    
        onMounted(() => {
          console.log(hw.value)
        })
    
        return {
          hw
        }
      }
    })
    </script>
    
    

    helloworld

  • 如果ref传递进去的是一个对象,那么它的操作方法和传递简单数据类型是不一样的,传递简单数据类型或者不传,返回的都是Ref,但是传递的是extends object的时候,返回的类型就变成了ToRef

那么我们就去看看它到底是怎么做的。

首先我们进入packages/reactivity/src/ref.ts文件,找到第41行

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

跟着createRef方法,我们接着往下看

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

这时候,重头戏就来了,这个RefImpl是什么

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

RefImpl是一个类,用于构建实例化对象,声明了valuegetset方法,在get的时候添加进行track,在set的时候进行trigger,关于tracktrigger,咱们等下再讨论。我们可以先看看TrackOpTypesTriggerOpTypes是什么

export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

其实很简单,就是两个字符串常量枚举。

除此之外,咱们还可以得到很重要的一个因素:convert,它在实例化的时候,对传递进来的参数,进行了操作,这是个什么?它做了什么?

我们可以看看它的源码

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

是不是非常简单。

  • 如果是对象,那么使用reactive进行响应式绑定,这也正好印证了我们上面从Ref类型上得出的结论3
  • 如果是基本数据类型,那么值还是他本身

ref其实就这么点东西

简单来说:

  1. ref不管传递进去的是什么,最终返回的都是一个对象,对象中有个value
  2. ref声明的引用数据类型,实际上是reactive来进行引用赋值
  3. getset的时候,会触发track以及trigger的事件来进行依赖收集
  4. ref这个api,实际上并没有走Proxy进行响应代理,而是直接使用了gettersetter,以及tracttrigger

reactive

按照惯例,从类型入手

export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;

export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>;

declare type UnwrappedObject<T> = {
    [P in keyof T]: UnwrapRef<T[P]>;
} & SymbolExtract<T>;

export declare type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;

declare type UnwrapRefSimple<T> = T extends Function | CollectionTypes | BaseTypes | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] ? T : T extends Array<any> ? {
    [K in keyof T]: UnwrapRefSimple<T[K]>;
} : T extends object ? UnwrappedObject<T> : T;

根据类型,我们依然可以得到几个结论

  • reactive只接收对象类型的参数
  • 如果reactive中传入的是ref包裹之后的对象,那么不进行代理,还是使用原对象
  • reactive类型中存在一些不需要进行代理的值,有以下判断
    • 如果是函数、集合(Map、Set、WeakMap、WeakSet)、基本数据类型、Ref、全局对象,那么不进行响应绑定
    • 如果是数组,那么数组原生的方法不进行响应绑定
    • 如果是对象,判断对象中的值是否是Ref,如果是,那么得到它的value并再次递归进行所有判断,如果不是,那么对其值进行所有递归判断。
    • 如果是对象,会对其内置的所有Symbol属性进行类型声明

接下来我们去看看它的代码,是怎么做的。我们在ref.ts文件的同级目录下,找到reactive.ts文件,点击进去。然后在第88行,可以找到reactive函数

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

我们可以看到,在这个函数中,做了两件事

  1. 判断传递进来的对象是否有ReactiveFlags.IS_READONLY这个属性,如果有,那么直接返回这个对象。

    根据名字,我们可以很直观的得到一个信息,这个字段是用来标识readonly的,我们找到这个ReactiveFlags,果然如此

    export const enum ReactiveFlags {
      SKIP = '__v_skip',
      IS_REACTIVE = '__v_isReactive',
      IS_READONLY = '__v_isReadonly',
      RAW = '__v_raw'
    }
    

    这是一个定义常量的枚举,用来为reactive添加各种标记。而且从语义可以直接了当的看到它们是干什么用的

    • SKIP —— 跳过,不进行代理
    • IS_REACTIVE —— 是否响应对象
    • IS_READONLY —— 是否只读
    • RAW —— 用来保存我们代理的原对象

    到这儿我们应该心里有个疑问,这个readonly是哪儿来的呢?肯定有个东西会添加readonly标签。果不其然,我们在第143行,找到了一个readonly函数

    export function readonly<T extends object>(
      target: T
    ): DeepReadonly<UnwrapNestedRefs<T>> {
      return createReactiveObject(
        target,
        true,
        readonlyHandlers,
        readonlyCollectionHandlers,
        readonlyMap
      )
    }
    

    它也使用了createReactiveObject这个函数,只不过第二个参数,传递的是相反的值。所以,我们大概可以推断出来,这个函数的作用,创建响应对象的工厂函数,可以根据传递的参数进行不同的配置。

  2. 使用createReactiveObject创建一个响应对象。

废话不多说,接下来我们看看creaetReactiveObject这个函数,我们可以在第161行找到它

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
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  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
}

先看参数

  • target —— 我们要代理的对象
  • isReadonly —— 是否只读,正好印证了我们上面的猜想
  • baseHandler —— 常规对象类型的操作
  • collectionHandlers —— 集合类对象的操作
  • proxyMap —— 当前代理对象类型的集合记录

baseHandlerscollectionHandlersproxyMap这三个,与本篇所讲关系不大,大概看看就好,我们只需要关注第一个target就好。

如果后续我有心情写这块的,大家可以继续接着看看。

参数看完我们继续来看这个函数内部做了什么工作

if (!isObject(target)) {
  if (__DEV__) {
    console.warn(`value cannot be made reactive: ${String(target)}`)
  }
  return target
}

如果传进来的不是一个对象,那么直接返回,不进行代理。

这正好印证了我们从它的类型中得出的结论的第一点

// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
  target[ReactiveFlags.RAW] &&
  !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
  return target
}

如果传进来的对象中,存在RAW属性,那么说明是已经代理过的对象,并且不是在一个响应对象中添加readonly属性的时候,直接返回。

其实这块本来应该是只判断RAW的,后来应该是出现了边缘case,所以添加了第二个条件

// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
  return existingProxy
}

在代理记录中寻找是否存在当前对象的代理,如果有,直接返回

使用缓存,不去再次进行代理,性能优化

// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
  return target
}

对传递进来的对象,进行分类,如果是INVALID,那么不进行代理。

我们可以看看getTargetType这个函数做了什么工作

const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

export const toRawType = (value: unknown): string => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1)
}

export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
  objectToString.call(value)

当我们将对象传递进去的时候,它首先判断是否是存在SKIP标签,或者对象不可以进行扩展,如果是的话,直接返回INVALID,不进行代理。否则判断是否是ObjectArrayMapSetWeakMapWeakSet这几个中的某个,如果不是,返回INVALID,否则返回对应的标签。

其实从上面的注释就可以看出来有一个白名单列表,只有那几种类型才会被代理。

// only a whitelist of value types can be observed.
const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy

终于到了人们所说的Proxy了。

在这里,对传进来的,白名单中的对象,进行了Proxy代理,并且根据类型标签,使用不同的handler,然后在代理的集合记录中将本次代理的对象进行缓存,供下次使用,实际上也就是上面说的那儿。然后将代理对象返回。

要区分handlers是因为集合类对象和常规的ObjectArrayProxy的时候有不同的表现,需要进行专门的操作,本篇不做详解。

reactive这个方法,也就这么多东西了,它的响应功能的实现,主要在handlersgettersetter中。

总结

  1. reactive只可以代理对象
  2. reactive包裹过的对象有_v__标识符
  3. reactive内部,使用了缓存来优化
  4. reactive有一个白名单,只有在这个白名单中的类型,才可以进行代理
  5. reactive对普通对象类型和集合类对象类型有不同的操作

handlers

handlers这块我给大家大概看一看它做了什么。。。主要它太长了,真要写比上面这些东西加起来都多。

我们直接来看代码(以baseHandlers中的mutableHandlers举例),来到同一文件夹下的baseHandlers.ts文件中,在201行找到mutableHandlers,在139行找到set,在40行找到get

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()

在这个文件中,导出了mutableHandlers,它是个对象。

这个handlers实际上就是我们在reactive函数中的baseHandlers,可以在本篇介绍reactive的时候,找到它,它是createReactiveObject方法的第三个参数。

createReactiveObjectnew Proxy的时候,我们将其传入Proxy的第二个参数。

也就是说,实际上这个东西,就是Proxy的第二个参数的封装,而我们所需要重点关注的getset就在这儿实现

get

我们可以看看createGetter函数做了什么

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, 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
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : isNonTrackableKeys(key)
    ) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    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.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

篇幅所限,在这儿就不给大家进行特别详细的讲解了,我们所要关注的,只有这儿一块代码,其他的都是各种case

if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

如果不是readonly,那么就通过track添加依赖收集

set

no bb,showCode

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

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

同样,我们只需要关注重点

if (hasChanged(value, oldValue)) {
  trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}

如果值发生了变化,那么就通过trigger进行触发

track

根据上面的refreactive的使用,我们可以得知,track是进行依赖收集的函数。

我们来到同目录下的effect.ts文件中,在141行找到tract函数

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

track函数接收三个参数

  • target —— 要收集依赖的对象
  • type —— 收集的依赖的类型
  • key —— 收集的key

我们还是分块来看这个函数做了什么

if (!shouldTrack || activeEffect === undefined) {
  return
}

这段代码就一个作用,拦截所有不合规的依赖收集。shouldTrack,从名字就可以得到,是否应该进行依赖收集,如果是否,那么就直接返回。activeEffect是当前响应的副作用函数,在effect中发挥着很重要的作用,在这儿不展开讲。

常规的响应,不需要考虑上面这段代码

let depsMap = targetMap.get(target)
if (!depsMap) {
  targetMap.set(target, (depsMap = new Map()))
}

我们首先看看targetMap是什么东西

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

targetMap是一个WeakMap,用来收集所有的响应对象依赖,可以简单的理解为一个依赖收集者。其实在这儿,通过类型,我们就可以直接得到vue3中响应式的依赖收集观察者的数据结构了,不过为了方便大家理解,还是等js代码结束之后再给大家描述吧。

在上面的代码中,首先根据传递进来的target对象,在targetMap中查找是否有依赖,如果没有,那么在targetMap中为其添加依赖列表。

我们可以得到一个外层结构

new WeakMap([
  [target, new Map]
])
let dep = depsMap.get(key)
if (!dep) {
  depsMap.set(key, (dep = new Set()))
}

继续往下看,在depsMap,也就是上面那个target的依赖收集列表中,查找是否存在当前key,如果不存在,那么添加一个当前key及一个Set列表。

在这一步,我们可以得到一个这样的数据结构

new weakMap([
  [
    target,
    new Map([
      [
        key,
        new Set()
      ]
    ])
  ]
])

其实到这一步,整体数据结构已经很清晰了,相信大家闭着眼睛都能猜到这个Set用来干嘛

if (!dep.has(activeEffect)) {
  dep.add(activeEffect)
  activeEffect.deps.push(dep)
  if (__DEV__ && activeEffect.options.onTrack) {
    activeEffect.options.onTrack({
      effect: activeEffect,
      target,
      type,
      key
    })
  }
}

activeEffect是指当前在执行的副作用函数。如果在当前target的当前key的依赖列表中不存在它,那么将其添加到依赖列表,同时副作用函数的依赖列表中也添加当前依赖列表。

如果是在开发环境,并且存在自定义track,那么就执行自定义track

众所周知,vuetemplate会被编译成render函数,而我们在template中使用的变量,会在render的时候用到,这个时候就会触发track,将当前targetkey添加到targetMap中,并将当前执行的函数添加到key对应的Set列表中。

trigger

同理,我们可以知道trigger函数是用来触发依赖响应的。

effect.ts文件的第167行,我们找到了trigger函数。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}

有一说一,这函数真长。不过也不用担心,咱们这次讨论的,只是常规对象的响应实现,所以,我为大家找几个比较关键的代码块一起来看看。

const depsMap = targetMap.get(target)
if (!depsMap) {
  // never been tracked
  return
}

根据target查找是否有对应的响应依赖,如果没有的话直接返回。

const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
  if (effectsToAdd) {
    effectsToAdd.forEach(effect => {
      if (effect !== activeEffect || effect.allowRecurse) {
        effects.add(effect)
      }
    })
  }
}

effects是一个Set集合,用于存储,本次trigger要执行的所有函数。

而下面的add方法,则是一个Set,也就是我们每个key对应的响应列表,使用add方法,将本次要执行的所有响应函数,添加到effects中。

if (key !== void 0) {
  add(depsMap.get(key))
}

如果当前target存在响应依赖,那么获取当前key的所有依赖函数并使用add添加到本次要执行的effects中。如果存在,返回值应该是个Set,否则会返回一个undefined。正好与上面的add函数的类型保持了一致。

这个是用于处理普通对象的。这句代码上下的其他方法,都是处理其他数据结构的case,本文不会重点关注。

const run = (effect: ReactiveEffect) => {
  if (__DEV__ && effect.options.onTrigger) {
    effect.options.onTrigger({
      effect,
      target,
      key,
      type,
      newValue,
      oldValue,
      oldTarget
    })
  }
  if (effect.options.scheduler) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

effects.forEach(run)

run函数,就是用来执行所有响应函数的。

在这个函数内部,首先判断是否是在开发环境,并且存在自定义trigger,如果存在那么会执行自定义trigger

紧接着有个scheduler,这是个调度器,在computed中会用到,本篇不展开讲。

最后就是执行每一个effect

总结

其实这么顺者api找下来,大多数人,还是会一脸懵逼,因为东西太乱,太杂。

所以接下来我为大家捋一遍顺序,结合这个顺序,再回去读一遍代码,就会有一个比较清晰的认知了。

  • template中使用了refVal响应对象,实际上这个template会被编译成render函数。
  • 在使用render函数的时候,会触发refValget方法,在get方法中,会调用track进行依赖收集
  • 在我们点击按钮的时候,执行了refVal.value += 1这个方法,会触发到refVal内部封装的set,同时触发trigger进行通知所有的依赖函数,并执行
  • trigger触发了绑定试图的render函数,render重新执行,我们在页面上看到的值就发生了变化

看着挺长的,其实原理很简单,就是通过get的时候添加依赖收集,在set的时候触发响应依赖。

结语

看源码实际上是一个挺有意思的过程,我觉得这是在有一定的基础的情况下提升自己代码风格以及质量最快也是最有效的方式。

你可以看到遇到这种场景,大神们是怎么做的,除了可以学习写代码的风格,还可以更近一步的学到一些业务中不会出现的东西,可以去更深入的思考,这么做的意义是什么。

本篇文章纯属个人见解,如有遗漏或者错误,请帮忙指出!

感谢各位能看到这里,我们下期再见!


技术分享宣传图@3x.png