Vue3.0 pre-alpha version was officially released on October 5, 2019, followed by more than 500 PRs and 1000 commits over the next few months, and finally Vue3.0 alpha.1 was released on January 4, 2020. the core code of Vue3.0 is basically complete, and the main work left so far is server-side rendering, which the Vue team is actively working on. The Vue team is also actively working on it. The responsive API code is basically stable and will not change much (the reactivity package in packages), so I will analyze the responsive principle of Vue3.0 from the source code.

Responsive API for Vue 3.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <div>{{ count }} {{ object.foo }}</div>
  <div>{{ plusOne }}</div>
</template>

<script>
import { ref, reactive, computed, watch, onMounted, onUpdated, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const plusOne = computed(() => count.value + 1)
    const object = reactive({ foo: 'bar' })
    watch(() => console.log(count.value))
    onMounted(() => console.log('mounted!'))
    onUpdated(() => console.log('updated!'))
    onUnmounted(() => console.log('onUnmounted!'))

    return {
      count,
      plusOne,
      object
    }
  }
}
</script>

Vue3.0 uses the setup function as the entry point for the entire component, uses the directly imported onXXX function to register lifecycle hooks, and uses the variables from the return in the page. There are some changes in the whole writeup compared to Vue 2.0, so we won’t discuss the lifecycle code related to the component here (it belongs to the packages/runtime-core package).

The most important thing in responsive is the remaining 4 APIs. ref and reactive are both methods to convert incoming parameters into responsive objects, the difference is that ref converts basic data types (string, number, bool, etc.) into responsive data, while reactive converts other data types into responsive data. To ensure that the basic data is responsive, ref wraps the basic data in a layer, so in the code above, you need to use count.value to get the value of count, but in the template, it is automatically unwrapped (unwrap), so you can use count directly. computed has the same role as vue2.0 and represents the computed property. watch is used to listen for state in the internal logic and will be executed once for each dependency change.

One of the features of Vue is data-driven view updates. By responsive, we mean that when data changes, the part of the view that needs to change is automatically updated. Unlike Vue 2.0, Vue 3.0 uses a monorepo structure that pulls the responsive code into a separate package - reactivity - which means you can reference this package separately in non-Vue projects to use responsive data.

Source code structure

Let’s explain the structure of the source code to make it clearer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// packages/reactivity
src
├── baseHandlers.ts  // Proxy 针对普通数据的 handler 函数的实现
├── collectionHandlers.ts  // Proxy 针对集合类型数据的 handler 函数的实现
├── computed.ts  // computed 的实现
├── effect.ts   // effect 的实现,watch 是基于 effect
├── index.ts  // 对外暴露所有 API
├── lock.ts  // 全局锁
├── operations.ts // 操作类型的常量
├── reactive.ts  // reactive 的实现
└── ref.ts  // ref 的实现

In fact, the code logic is basically in baseHandlers.ts , collectionHandlers.ts , computed.ts , effect.ts , reactive.ts , ref.ts which are 6 files. index is the file that exposes the API to the public, lock contains the variables and methods that can modify the global responsiveness, and operations are the constants corresponding to the types of dependency changes triggered by method hijacking. Of course, there are some utility functions used here, all of which come from packages/shared, and I will analyze them directly in the code when I explain them later.

Data hijacking

Vue2.0 uses Object.defineProperty for data hijacking, which actually proxies each property of the object during initialization, while Vue3.0 uses a combination of Proxy and Reflect to proxy the entire object directly. The difference is that in Vue 3.0 you no longer need to add and delete properties responsively via the vm.$set and vm.$delete methods, you don’t need to override the array’s native methods, and listening to the array doesn’t break the JS engine’s rendering, which results in better performance.

Data Interception for Proxy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const data = { name: 'kpl' }
const handler = {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
}
let p = new Proxy(data, handler)
p.name = '123'
console.log(data) // { name: '123' }

The specific usage of Proxy and Reflect can be seen in Mr. Nguyen’s ES6 Primer. Why doesn’t the get function in Proxy return the intercepted property directly, but instead calls the Reflect API?

First, Proxy supports 13 methods for interception, and Reflect has the same 13 methods. These methods include some language-internal methods (such as Object.defineProperty) that sometimes throw errors during use, and Reflect returns false in this case. The Proxy object can easily call the corresponding Reflect method to complete the default behavior as a basis for modifying the behavior. That is, no matter how Proxy modifies the default behavior, you can always get the default behavior on Reflect.

Vue3.0 Responsive Schematic

I combed through the flow of data proxying, method hijacking, dependency collection and triggering in Vue 3.0 and drew the following schematic. It may be a little bit roundabout at first, but you will have a clearer understanding when you look at this diagram after reading the source code parsing behind it.

vue

Reactive

Although Proxy can directly proxy objects, it can only proxy one layer of properties, and you need to manually recursively implement it for deeper detection inside objects. Of course, recursive Proxy has performance pitfalls. How does Vue3.0 avoid excess performance loss?

Vue3.0 caches the set of mapping relationships between the original data and the proxy data to prevent the same data from being repeatedly proxied. Also, when generating responsive data using reactive, it is not recursive, and only when the trap of get is triggered by accessing the responsive data, it nests recursive properties for proxy hijacking (unlike Vue2.0 where dependency collection is done at initialization, which will be explained in detail in the handler function).

1
2
3
4
5
6
7
8
const rawToReactive = new WeakMap<any, any>()  // 原始数据 -> 响应式数据
const reactiveToRaw = new WeakMap<any, any>()  // 响应式数据 -> 原始数据
const rawToReadonly = new WeakMap<any, any>()  // 原始数据 -> 只读响应式数据
const readonlyToRaw = new WeakMap<any, any>()  // 只读响应式数据 -> 原始数据
const readonlyValues = new WeakSet<any>()      // 手动标记的只读数据集合
const nonReactiveValues = new WeakSet<any>()   // 不可响应式数据集合
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])  // 集合数据类型
const isObservableType = makeMap('Object,Array,Map,Set,WeakMap,WeakSet')  // 判断传入的对象是否为响应式数据类型

Why do we use WeakMap and WeakSet to hold mapped data collections? Because WeakMap and WeakSet hold objects that are weakly referenced, and the garbage collection mechanism will release the memory occupied by the referenced object as soon as all other references to the object are cleared. Therefore, using these two data structures can better reduce memory overhead, in addition to having a higher search query efficiency.

Vue 3.0 uses three main responsive methods, reactive , readonly and shallowReadonly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  if (readonlyToRaw.has(target)) return target  // 如果是只读响应式数据,直接返回
  if (readonlyValues.has(target)) return readonly(target) // 如果是手动标记的只读数据,使用 readonly 去代理
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

export function readonly<T extends object>(target: T): Readonly<UnwrapNestedRefs<T>> {
  if (reactiveToRaw.has(target)) target = reactiveToRaw.get(target) // 如果已经是响应式数据,则使用原始数据
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}

export function shallowReadonly<T extends object>(target: T): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    shallowReadonlyHandlers,
    readonlyCollectionHandlers
  )
}

Here you can see that all three methods actually call the createReactiveObject method, and we’ll take a look at this method later. reactive returns responsive data, readonly returns read-only responsive data, so what does the shallowReadonly method do? It returns an object with only the outermost read-only responsive data, and it does not recursively proxy the internal data responsively. It is mainly used for props proxy objects created in stateful components.

Let’s start with a few of the tool functions that will be used in the later methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 判断是否是对象
export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'

// 判断是否为可观察数据
const canObserve = (value: any): boolean => {
  return (
    !value._isVue &&
    !value._isVNode &&
    isObservableType(toRawType(value)) &&
    !nonReactiveValues.has(value)
  )
}

In fact, I have written a lot of TS code in my projects, but after reading the source code, I realized that my TS is only half-assed, and I learned a lot from it. The isObject function uses the top-level type unknown for the input, not any, to avoid arbitrary operations on the input (which cannot be assigned by types other than unknown and any). Also, the function return type uses a type predicate, Record<any, any> instead of object, because TS allows access to arbitrary properties of objects of type Record<any, any> without errors. canObserve specifies which data is observable. Non-Vue components, non-dom nodes, of the type defined by isObservableType and not unresponsive data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) return target      // 如果不是对象,直接返回原始数据
  let observed = toProxy.get(target)        // 获取 target 对应的响应式数据
  if (observed !== void 0) return observed  // 如果存在响应式数据(已经被代理过),直接返回响应式数据
  if (toRaw.has(target)) return target      // 如果 target 是响应式数据,直接返回 target
  if (!canObserve(target)) return target    // 如果target 不可被观察,直接返回 target
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)    // 集合数据类型使用 collectionHandlers,其它类型使用 baseHandlers
  toProxy.set(target, observed)             // 缓存原始数据 -> 响应式数据的映射关系
  toRaw.set(observed, target)               // 缓存响应式数据 -> 原始数据的映射关系
  return observed
}

Although the code is not much, we still have several questions.

  1. what does void 0 mean? Actually it is undefined. The source code uses void 0 instead of undefined, first of all because undefined is rewritten under local scope, while void is not rewritten and void 0 has fewer bytes. In fact, many JavaScript compression tools replace undefined with void 0 in the compression process.

  2. Why do we use different handler functions for different data types? Vue 3.0 uses two files, baseHandlers and collectionHandlers, to handle handler functions. The collectionHandlers file is dedicated to handling collection type data (Map, Set, WeakMap, WeakSet), why do collection type data need separate handler functions? Because these collection types use the so-called “internal slots” to access properties directly through the built-in method (this), not through [[Get]]/[[Set]], which the Proxy cannot intercept. This is because after using Proxy to proxy a collection type, this=Proxy, not the original object, will not be accessible. So we need to do a layer of function hijacking and just modify the pointing of this to the original mapping.

baseHandlers

baseHandlers.ts exposes a total of three methods, mutableHandlers , readonlyHandlers and shallowReadonlyHandlers. This corresponds to the three responsive methods mentioned above, reactive , readonly and shallowReadonly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// LOCKED 是一个全局开关,锁住情况下数据不可变
export let LOCKED = true
export const lock = () => LOCKED = true
export const unlock = () => LOCKED = false

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

export const readonlyHandlers: ProxyHandler<object> = {
  get: createGetter(true),
  set: createSetter(true),
  has,
  ownKeys,
  deleteProperty(target: object, key: string | symbol): boolean {
    if (LOCKED) { // 数据被锁住的情况
      return true
    } else {
      return deleteProperty(target, key)
    }
  }
}

export const shallowReadonlyHandlers: ProxyHandler<object> = {
  ...readonlyHandlers,
  get: createGetter(true, true),
  set: createSetter(true, true)
}

In fact, you can see that the three exposed handler methods actually only hijack the get, set, has, ownKeys and deleteProperty methods. get and set are easy to understand, has actually hijacks the propKey in proxy operation, ownKeys can hijack Object.getOwnPropertyNames(proxy) , Object.getOwnPropertySymbols(proxy ) , Object.keys(proxy) , for... .in method, deleteProperty hijacks the delete proxy[propKey] method.

Get

Let’s look at the get hijacking method createGetter :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export const isArray = Array.isArray
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'
export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => hasOwnProperty.call(val, key)
export function isRef(r: any): r is Ref {
  return r ? r._isRef === true : false
}
export function toRaw<T>(observed: T): T {
  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed  // 获取原始数据
}

// ES6 提供了 11 个内置的 Symbol 值,指向语言内部使用的方法
const builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol)
    .map(key => (Symbol as any)[key])
    .filter(isSymbol)
)

// 数组的 includes, indexOf, lastIndexOf 三个方法不走代理
const arrayIdentityInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayIdentityInstrumentations[key] = function(
    value: unknown,
    ...args: any[]
  ): any {
    // 获得原始数据并执行原生方法
    return toRaw(this)[key](toRaw(value), ...args)
  }
})

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 数组的 includes, indexOf, lastIndexOf 三个方法不走代理,同时不收集依赖
    if (isArray(target) && hasOwn(arrayIdentityInstrumentations, key)) {
      return Reflect.get(arrayIdentityInstrumentations, key, receiver)
    }
    const res = Reflect.get(target, key, receiver)
    // ES6 内置的 Symbol 属性无法被收集依赖,这里直接返回值,不再收集依赖
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // 如果是 shallowReadonly,不需要递归收集内层依赖,只收集最外层依赖
    if (shallow) {
      track(target, TrackOpTypes.GET, key)  // 收集依赖
      return res
    }
    // 如果是 Ref 数据,直接返回原始数据的值(Ref 的结构会在后文分析)
    if (isRef(res)) {
      return res.value
    }
    track(target, TrackOpTypes.GET, key)  // 收集依赖
    return isObject(res)
      ? isReadonly
        ? readonly(res)
        : reactive(res)
      : res
  }
}

The processing functions for get interceptions are still relatively clear.

When I first saw the arrayIdentityInstrumentations function, I didn’t understand what its role was, so I had to go to the repository and check the corresponding commit. From that single test, we found that since the three methods includes , indexOf , lastIndexOf all use strict equality to determine the relationship of the found elements, if the responsive data (array) is pushed into a reference type of data, using the above three methods will find no match to the added data. Therefore, it is possible to get the correct matching relationship without proxying these three methods.

After that, the built-in Symbol property of ES6 does not collect dependencies, the data of shallowReadonly is only responsively proxied to the outermost layer, and the data of Ref is not recursively processed because it is a wrapper for basic type data and there is no nested data inside, so the rest needs to be manually recursively proxied. Vue 3.0 collects dependencies through the track function, which will be analyzed when we talk about the effect file.

Set

Let’s look at the set hijacking method createSetter :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
export const hasChanged = (value: any, oldValue: any): boolean =>
  value !== oldValue && (value === value || oldValue === oldValue) // 排除 NaN 的干扰
  
function createSetter(isReadonly = false, shallow = false) {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
    if (isReadonly && LOCKED) {  // 如果是只读响应式数据或被锁住,则直接返回
      return true
    }

    const oldValue = (target as any)[key]  // 获取旧值
    if (!shallow) { // 非 shallowReadonly 时
      value = toRaw(value)  // 获得新值的原始数据
      if (isRef(oldValue) && !isRef(value)) {  // 如果旧值是 Ref 数据,新值不是,就直接更新旧值数据的 value,并返回
        oldValue.value = value
        return true
      }
    }

    const hadKey = hasOwn(target, key)  // 判断响应式数据上是否有这个 key
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {  // 如果是原始数据原型链上的数据操作,不做任何触发监听函数的行为。
      if (__DEV__) {  // 开发环境会多传额外信息
        const extraInfo = { oldValue, newValue: value }
        if (!hadKey) {
          trigger(target, TriggerOpTypes.ADD, key, extraInfo)
        } else if (hasChanged(value, oldValue)) {
          trigger(target, TriggerOpTypes.SET, key, extraInfo)
        }
      } else {
        if (!hadKey) {  // 如果不存在该 key,触发新赠属性操作
          trigger(target, TriggerOpTypes.ADD, key)
        } else if (hasChanged(value, oldValue)) {  // 如果存在该 key,触发更新属性操作
          trigger(target, TriggerOpTypes.SET, key)
        }
      }
    }
    return result
  }
}

The source code of createSetter is quite easy to understand with the official comments, and Vue 3.0 implements dependency triggering through trigger (which will also be analyzed in the effect file). Beyond that, there are of course a few confusing points.

  1. why is there (value === value || oldValue === oldValue) logic in the hasChanged function? Because NaN ! == NaN, so this logic is added to exclude the interference of NaN.

  2. why isRef(oldValue) && !isRef(value) does not need to trigger the dependency? Because the Ref data structure itself has the logic to hijack the set function (which triggers the dependency), so there is no need to trigger the dependency again.

  3. target === toRaw(receiver) What does this logic mean?

// don’t trigger if target is something up in the prototype chain of original

There is this comment on the source code that means don’t do anything to trigger the listener function if it is a data operation in the prototype chain of original data. Still don’t quite understand, so I commented out this line, ran through the single test, and got this test case, I finally understood it. The receiver is generally an object that has been proxied by the Proxy, but the handler’s set method may also be called indirectly in the prototype chain or in some other way (so not necessarily by the proxy itself).

1
Object.setPrototypeOf(child, parent) // child.__proto__ === parent true

If child and parent are 2 Proxy proxies, target is not equal to toRaw(receiver) for child. A set operation on child should not change the data on parent, so no listener function will be triggered for data operations on the original data prototype chain.

deleteProperty, has, ownKeys

There are three more functions to hijack. The source code is very simple, add a few lines of comments, no more analysis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
  if (result && hadKey) {  // 存在指定 key 且不报错情况触发删除依赖项的执行
    if (__DEV__) {
      trigger(target, TriggerOpTypes.DELETE, key, { oldValue })
    } else {
      trigger(target, TriggerOpTypes.DELETE, key)
    }
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)  // 触发依赖
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY) // 触发依赖
  return Reflect.ownKeys(target)
}

collectionHandlers

As already mentioned, the four data types Map, Set, WeakMap, WeakSet Proxy can not properly intercept all properties. For example, the proxy set, delete and other methods will directly report an error, but of course the get method of access can still be intercepted normally. Therefore, we can implement a new object, which has all the APIs corresponding to the set data type, and just hijack the proxy to this new object by get.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function createInstrumentationGetter(instrumentations: Record<string, Function>) {
  return (target: CollectionTypes, key: string | symbol, receiver: CollectionTypes) =>
    Reflect.get(  // 如果新对象有该 key 且原始数据中也有该 key,就代理到新对象,否则使用原始数据
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(mutableInstrumentations)  // 代理到可变的新对象
}

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(readonlyInstrumentations)  // 代理到只读的新对象
}

The next step is to look at the internal implementation of the two new objects mutableInstrumentations and readonlyInstrumentations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size(this: IterableCollections) {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false)
}

const readonlyInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReadonly)
  },
  get size(this: IterableCollections) {
    return size(this)
  },
  has,
  add: createReadonlyMethod(add, TriggerOpTypes.ADD),
  set: createReadonlyMethod(set, TriggerOpTypes.SET),
  delete: createReadonlyMethod(deleteEntry, TriggerOpTypes.DELETE),
  clear: createReadonlyMethod(clear, TriggerOpTypes.CLEAR),
  forEach: createForEach(true)
}

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method as string] = createIterableMethod(method, false)
  readonlyInstrumentations[method as string] = createIterableMethod(method, true)
})

You can see that in the new object is a proxy for get , size , has , add , set , delete , clear , forEach and some methods related to iterators (keys, values, entries, Symbol.iterator).

Let’s start with a few instrumental functions.

1
2
3
4
5
6
7
8
export function toRaw<T>(observed: T): T {
  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
export const hasChanged = (value: any, oldValue: any): boolean =>
  value !== oldValue && (value === value || oldValue === oldValue) // 排除 NaN 的干扰
const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value
const toReadonly = <T extends unknown>(value: T): T => isObject(value) ? readonly(value) : value
const getProto = <T extends CollectionTypes>(v: T): any => Reflect.getPrototypeOf(v)

The first two methods are described in the previous section, toReactive converts raw data into mutable responsive data, toReadonly converts raw data into read-only responsive data, and getProto reads the object’s proto property (to get the prototype object).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function get(target: MapTypes, key: unknown, wrap: typeof toReactive | typeof toReadonly) {
  target = toRaw(target)  // 获得原始数据
  key = toRaw(key)  // 获得 key 的原始数据(key 也可能为响应式对象)
  track(target, TrackOpTypes.GET, key)
  return wrap(getProto(target).get.call(target, key))  // 获取原始数据的 key 值并转化为响应式数据
}

...  // size, has, add, set, delete, clear 等方法的逻辑都跟前面的一致,很好理解,这里就不再赘述

function createForEach(isReadonly: boolean) {
  return function forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
    const observed = this
    const target = toRaw(observed)  // 获取原始数据
    const wrap = isReadonly ? toReadonly : toReactive
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    // 将 callback 的数据都转化成响应式数据
    function wrappedCallback(value: unknown, key: unknown) {
      return callback.call(observed, wrap(value), wrap(key), observed)
    }
    return getProto(target).forEach.call(target, wrappedCallback, thisArg)
  }
}

function createIterableMethod(method: string | symbol, isReadonly: boolean) {
  return function(this: IterableCollections, ...args: unknown[]) {
    const target = toRaw(this)
    const isPair = method === 'entries' || (method === Symbol.iterator && target instanceof Map)  // 判断是否为 key/value 结构
    const innerIterator = getProto(target)[method].apply(target, args)  // 获取原型链上迭代器的方法
    const wrap = isReadonly ? toReadonly : toReactive
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    return {
      next() {
        const { value, done } = innerIterator.next()
        return done  // 迭代器执行到最后一个时值是 { value: undefined, done: false } 不需要再转化成响应式数据
          ? { value, done }
          : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), done }
      },
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

Although the collectionHandlers.ts file is much longer than baseHandlers.ts, it is much easier to understand the code of baseHandlers.ts before looking at the code of collectionHandlers.ts. collectionHandlers.ts creates a new object to hijack all the collection type data, so inside the hijack function you always get the original data and the prototype method of the original data first, and then bind that method to the original data to call it.

Reactive Summary

reactive implements data proxying through ES6’s Proxy and Reflect APIs to convert to responsive data, which naturally has the advantage of better performance and ease of use, but the disadvantage of not supporting browsers below IE11. It also avoids performance problems caused by nested recursive new Proxy through lazy access, unlike Vue2.0 where all dependencies are collected at initialization.

Ref

As already mentioned, reactive cannot convert basic data types, and Ref solves the problem of basic data types not being converted to responsive data by a layer of wrapping.

Let’s look at the data type of Ref.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const isRefSymbol = Symbol()

export interface Ref<T = any> {
  [isRefSymbol]: true  // 私有属性用来区分带有 value 字段的普通对象,且不想暴露给用户
  value: UnwrapRef<T>
}

export function isRef(r: any): r is Ref {
  return r ? r._isRef === true : false
}

Although there is a private property in Ref that determines whether the target object is a Ref structure, looking up the symbol property on an arbitrary object is much slower than a normal property, so isRef actually determines whether it is Ref data by the _isRef property, which is added when Ref is generated. The value property of Ref is the “unwrapped” type, which is actually determined by the recursive infer to achieve this.

This is a brief look at infer, because the unwrapper type is used throughout the Reactive and Ref files, and it can be difficult to read the source code without knowing something about it.

1
type ParamType<T> = T extends (param: infer P) => any ? P : T;

In this conditional statement T extends (param: infer P) => any ? P : T, infer P represents the function parameter to be inferred.

The whole statement means that if T can be assigned to (param: infer P) => any, the result is P in type (param: infer P) => any, otherwise it returns T.

Next, see an example.

1
2
3
4
5
6
7
8
9
interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string

In TypeScript 2.8 and later, after the introduction of the infer feature, there are also many built-in mapping types related to infer, such as ReturnType , ConstructorParameters , InstanceType and so on. Next, let’s see how the unwrapped types look like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type UnwrapArray<T> = { [P in keyof T]: UnwrapRef<T[P]> }
type BaseTypes = string | number | boolean

// 递归解包装 Ref 类型
export type UnwrapRef<T> = {
  // 如果是 ComputedRef 类型,继续解包装
  cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
  // 如果是 Ref 类型,继续解包装
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  // 如果是数组类型,对数组每一项进行解包装
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
  // 如果是对象类型,对对象每一项进行解包装
  object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
  ? 'cRef'
  : T extends Array<any>
    ? 'array'
    : T extends Ref | Function | CollectionTypes | BaseTypes  // 函数,集合类型和基本类型不需要解包装
      ? 'ref'
      : T extends object ? 'object' : 'ref']

We know from the code that the value structure of Ref can be of any type, but it must not be nested by a Ref type, be it Ref<Ref<T>> or Array<Ref>, { [key]: Ref } and so on.

After that, let’s see how to generate Ref data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val

export function ref(value?: unknown) {
  if (isRef(value)) {  // 如果已经是 Ref 数据,直接返回该数据
    return value
  }
  value = convert(value)  // 将 value 转化为响应式数据
  const r = {
    _isRef: true,
    get value() {
      track(r, TrackOpTypes.GET, 'value')  // 收集依赖
      return value
    },
    set value(newVal) {
      value = convert(newVal)
      trigger(r, TriggerOpTypes.SET, 'value', __DEV__ ? { newValue: newVal } : void 0)  // 触发依赖
    }
  }
  return r
}

When generating Ref, the attribute _isRef is added to the isRef function to identify it. Ref also internally intercepts the get and set functions, which corresponds to the createGetter function in Reactive.ts, which does not collect dependencies on data of type Ref and returns its value directly.

There are also 2 Ref related methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将目标对象里的每个 key 都转成 Ref 结构
export function toRefs<T extends object>(object: T): { [K in keyof T]: Ref<T[K]> } {
  if (__DEV__ && !isReactive(object)) {  // 必须是响应式数据才可以
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = {}
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>(object: T, key: K): Ref<T[K]> {
  return {
    _isRef: true,
    get value(): any {
      return object[key]  // 不收集依赖
    },
    set value(newVal) {
      object[key] = newVal  // 不触发依赖
    }
  } as any
}

Convert a reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.

As you can see from the official documentation, toRefs is used to convert responsive data into normal objects, but the properties of the resulting objects are of type Ref and still have responsiveness.

Why does the Ref object returned in the toProxyRef function have no collection dependencies or trigger dependencies?

As mentioned before, no dependencies are collected or triggered for Ref types in reactive, so doesn’t the value returned by toRefs have responsiveness? Look at the following example.

1
2
const a = reactive({ x: 1, y: 2 })
const { x, y } = toRefs(a)

In fact, x and y are already proxied to a’s x and y properties, so when accessing x and y or changing their values, a’s set value() and get value() functions will be triggered to collect and trigger the dependencies. Therefore, vue3 deliberately removes the trigger and track functions from the Ref data returned by toRefs to prevent duplicate collection and triggering of dependencies.

Effect

Both computed and watch are wrapped based on effect. In this file, we focus on collecting and triggering dependencies.

Let’s look at the type declaration first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
export interface ReactiveEffect<T = any> {  // 监听函数
  (): T  // ReactiveEffect 是一个函数,没有入参,返回值为 T
  _isEffect: true  // 判断是否为 effect 的标识
  active: boolean  // 激活开关
  raw: () => T  // 依赖函数的原始函数
  deps: Array<Dep>  //  存储的依赖
  options: ReactiveEffectOptions  // 依赖函数的配置项
}

export interface ReactiveEffectOptions {
  lazy?: boolean  // 延迟执行的标志
  computed?: boolean  // 是否是 computed 的监听函数
  scheduler?: (run: Function) => void  // 自定义的依赖执行函数
  onTrack?: (event: DebuggerEvent) => void  // 调试中收集依赖时执行
  onTrigger?: (event: DebuggerEvent) => void  // 调试中触发依赖时执行
  onStop?: () => void  // 调试中触发 `stop` 函数时执行
}

Then look directly at the code for effect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ ? Object.freeze({}) : {}
export function isEffect(fn: any): fn is ReactiveEffect => fn != null && fn._isEffect === true  // 判断是否为 effect
function cleanup(effect: ReactiveEffect) {  // 清空 effect 里面的依赖项
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

export function effect<T = any>(fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> {
  if (isEffect(fn)) {  // 如果是 effect 函数,则使用它的原始函数
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)  // 创建监听函数
  if (!options.lazy) {  // 如果没有延迟执行的标志,先执行一次 effect 函数
    effect()
  }
  return effect
}

export function stop(effect: ReactiveEffect) {
  if (effect.active) {  // 如果 effect 处于激活状态
    cleanup(effect)  // 清空该监听函数的所有依赖
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false  // 关闭激活开关
  }
}

Here you have seen the role of several properties in the previous effect configuration item, and then you have to see how to create the listener function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const effectStack: ReactiveEffect[] = []  // 存放所有监听函数
export let activeEffect: ReactiveEffect | undefined

function createReactiveEffect<T = any>(fn: () => T, options: ReactiveEffectOptions): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effect.active) {  // 如果激活开关被关,执行原始函数
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {  // 如果 effectStack 里没有该监听函数,则收集依赖
    cleanup(effect)  // 清空该 effect 的依赖项
    try {
      effectStack.push(effect)
      activeEffect = effect  // 将 effect 保存到缓存区
      return fn(...args)  // 执行原始函数
    } finally {
      effectStack.pop()  // 执行完函数后,将 effect 从全局监听函数的栈中移除
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
}

If the effect is active, it will be put in the global effectStack, and if the value of the responsive data is changed or accessed during the execution of the original function, it will be triggered and collected by trigger and track.

There is a question here, every time the effect function is executed, it is first pushed into the effectStack and then popped after execution, so under what circumstances does effectStack.includes(effect) === true? From this single test, we know that it happens when there is a cyclic dependency on the execution function of effect, so we need to make sure that the dependency collection and triggering still works when there is a cyclic dependency.

Let’s take a look at how the trigger and track functions are implemented.

Track

track is the dependency collection function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Dep = Set<ReactiveEffect>  // 依赖集合
type KeyToDepMap = Map<any, Dep>  // 原始数据的属性映射的依赖集合
const targetMap = new WeakMap<any, KeyToDepMap>()  // 原始数据映射的依赖集合

let shouldTrack = true  // 标志是否收集
export function pauseTracking() {  // 暂停收集
  shouldTrack = false
}
export function resumeTracking() {  // 恢复收集
  shouldTrack = true
}

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return  // 如果是暂停收集或是未激活状态直接返回
  }
  let depsMap = targetMap.get(target)  // 获取原始数据的依赖集合
  if (depsMap === void 0) {  // 若该原始数据没有对应的依赖集合,则设置一个空的 Map
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)  // 获取原始对象指定 key 的依赖集合
  if (dep === void 0) {  // 若该 key 没有对应的依赖集合,则设置一个空的 Set
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {  // 若依赖集里没有执行原始函数前保存在缓存区的 effect
    dep.add(activeEffect)  // 添加这个依赖
    activeEffect.deps.push(dep)  // 缓存区里的 effect 同样在内部存储该依赖集
    if (__DEV__ && activeEffect.options.onTrack) {  // 开发环境触发钩子函数
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

targetMap is where the dependencies are stored globally, in a three-level nested structure of raw data -> properties -> dependency collections.

Why do we need deps inside each effect to store dependencies when we already have targetMap as the global structure for storing dependencies? The answer is given in the createReactiveEffect function earlier. Each time the function is executed, when the dependency does not exist in the effectStack, the cleanup function is executed to clear the dependency mapping from the targetMap using the internal deps. The same is true when the stop function is executed.

So here’s another question, why do we need to clear our own dependencies before each function execution? From this single test we know that when there is a conditional branch within a function, each execution may cause the dependency data to be different, so we need to collect the dependencies again before each execution.

Trigger

trigger is the function whose dependency is triggered.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
export const ITERATE_KEY = Symbol('iterate')

export function trigger(target: object, type: TriggerOpTypes, key?: unknown, extraInfo?: DebuggerEventExtraInfo) {
  const depsMap = targetMap.get(target)  // 获取原始数据的依赖集合
  if (depsMap === void 0) {  // 如果原始数据没有依赖,则直接返回
    return
  }
  const effects = new Set<ReactiveEffect>()  // 普通 effect 需要执行的依赖队列
  const computedRunners = new Set<ReactiveEffect>()  // computed 类型的 effect 需要执行的依赖队列
  if (type === TriggerOpTypes.CLEAR) {  // collectionHandlers 里的 clear 函数
    depsMap.forEach(dep => {  // clear 函数会将原始数据里的 key 清除,因此需要将内部所有 key 的依赖收集依赖队列里
      addRunners(effects, computedRunners, dep)
    })
  } else {
    if (key !== void 0) {  // type 为 SET | ADD | DELETE 
      addRunners(effects, computedRunners, depsMap.get(key))  // 将该 key 的依赖收集到依赖队列里
    }
    // 如果是 Add 或 DELETE 类型,会改变数据的数量,需要新增数组长度或迭代器的监听方法
    if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
      const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  computedRunners.forEach(run)  // 执行所有 computed 类型的依赖队列中的方法
  effects.forEach(run)  // 执行所有普通类型的依赖队列中的方法
}

function addRunners(effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined) {
  if (effectsToAdd !== void 0) {  // 如果要放入队列里的依赖不为空
    effectsToAdd.forEach(effect => {
      if (effect.options.computed) {  // 如果是 computed 类型,放到 computedRunners 中
        computedRunners.add(effect)
      } else {  // 如果是正常类型,放到 effects 中
        effects.add(effect)
      }
    })
  }
}

function scheduleRun(effect: ReactiveEffect, target: object, type: TriggerOpTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo) {
  if (__DEV__ && effect.options.onTrigger) {  // 开发环境触发对应钩子函数
    const event: DebuggerEvent = { effect, target, key, type }
    effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
  }
  if (effect.options.scheduler !== void 0) {  // 如果有自定义的依赖执行函数,传入依赖执行该方法
    effect.options.scheduler(effect)
  } else {  // 否则直接执行依赖函数
    effect()
  }
}

trigger is a process that executes dependencies by maintaining a queue of effects and computedRunners when responsive data changes, and then calling scheduleRun afterwards.

Effect Summary

effect describes how to collect dependencies, manage dependencies and trigger dependencies. Each time a listener is executed, it is placed in the effectStack queue, cached as activeEffect, popped out of the effectStack queue when execution is complete, and the activeEffect value is changed to the last listener in the effectStack queue. The listener function is put into the dependency of the accessed responsive data by the track function when it is executed for the first time and is saved in the targetMap collection. When the responsive data is modified, it is triggered by the trigger function, and the corresponding dependencies are taken out of the targetMap and put into the computedRunners and effects dependency execution queues according to the listener’s category. After that, the listeners in the computedRunners and effects queues are executed in this order.

Computed

Let’s start with a few instrumental functions and type declarations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export const isFunction = (val: unknown): val is Function => typeof val === 'function'  // 判断是否为函数
export const NOOP = () => {}

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: UnwrapRef<T>
}

export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}

export type ComputedGetter<T> = () => T
export type ComputedSetter<T> = (v: T) => void

export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

As you can see from the type declaration ComputedRef is a read-only Ref that has effect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export function computed<T>(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {  // 如果传入的参数是函数,那么就直接赋值给 getter
    getter = getterOrOptions
    setter = __DEV__
      ? () => { console.warn('Write operation failed: computed value is readonly') }
      : NOOP
  } else {  // 尽管 computed 是只读的,但是若传入了 get 和 set 的配置函数,也是可以手动改变其数据
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true  // 是否应该收集依赖的标志
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true,  // 延迟执行,初始化时不执行传入的函数
    computed: true,
    scheduler: () => {
      if (!dirty) {  // 只有有效访问计算属性才触发依赖
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    _isRef: true,
    effect: runner,
    get value() {
      if (dirty) {  // 多次同时访问计算属性只有第一次会触发依赖
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

As you can see from the source code, the value that goes into computedRef is read-only, but we can also manually change the value of the computed property after passing in custom set and get functions using computed. (This is consistent with vue2.0, which also provides a custom set method to change the computed property)

By using the dirty flag in the source code, we can avoid the problem of repeatedly triggering dependencies on computed properties. This is because the getter function of computed is triggered when a computed property and a computed property that depends on it are accessed at the same time, which would lead to multiple executions of trigger if the dirty flag bit did not exist. You can see this single test.

Summary

The reason we didn’t explain the watch API here is that it is actually part of the runtime code (packages/runtime-core/src/apiWatch.ts), but it is actually underpinned by effect, so we can get an idea of what watch does and how it works. Of course, those who want to understand the watch API thoroughly are advised to look at the runtime code, which is still more extended than effect.

After reading through the article, you can go back to the schematic above to get a deeper understanding.

In fact, the whole process of reading the source code was not easy and took a lot of effort. This includes a lot of advanced usage of TS, figuring out the author’s intention, and so on. At the same time, I also read some excellent source code analysis articles on the Internet, and sometimes when I can’t understand a certain piece of logic, I can see other people’s analysis and ideas and immediately be enlightened. Of course, there will be a lot of wrong interpretation, you have to determine their own context with the code.

Read the source code also has a lot of skills, the first is to understand the author’s comments, and then to make reasonable use of the single test, such as a piece of logic commented out, run a single test to see which sample hung, you can go to “guess” the role of the commented out logic. Vue’s projects have always had good commit specifications, making the code highly readable, which is great. I have also promoted Angular’s commit specification - commitizen in my team before. If a commit is merged in through a PR, you can also go to the repository and look through the PR, where the author of the PR will clearly state what the PR does.

Vue3.0 is rewritten using TypeScript, replacing Object.defineProperty with Proxy for data detection, improving responsive performance and allowing users to manipulate data more freely. At the same time, the Virtual DOM is refactored to adopt the idea of “combining motion and static”, which greatly improves the update performance of vdom.

A few months have passed since I started reading the source code to finish this article, and Vue3 has entered the subsequent optimization and finishing work. I am very happy to finally finish this article, of course, there is a lot of content belongs to the author’s speculation, if you have questions about any of the code analysis of this module in the statement or have a correction, you are very welcome to contact me, I will correct as soon as possible!