Vue3 原始碼之 reactivity

rmlzy發表於2021-01-29

注: 為了直觀的看到 Vue3 的實現邏輯, 本文移除了邊緣情況處理、相容處理、DEV環境的特殊邏輯等, 只保留了核心邏輯

vue-next/reactivity 實現了 Vue3 的響應性, reactivity 提供了以下介面:

export {
  ref, // 代理基本型別
  shallowRef, // ref 的淺代理模式
  isRef, // 判斷一個值是否是 ref
  toRef, // 把響應式物件的某個 key 轉為 ref
  toRefs, // 把響應式物件的所有 key 轉為 ref
  unref, // 返回 ref.value 屬性
  proxyRefs,
  customRef, // 自行實現 ref						
  triggerRef, // 觸發 customRef
  Ref, // 型別宣告
  ToRefs, // 型別宣告
  UnwrapRef, // 型別宣告
  ShallowUnwrapRef, // 型別宣告
  RefUnwrapBailTypes // 型別宣告
} from './ref'
export {
  reactive, // 生成響應式物件
  readonly, // 生成只讀物件
  isReactive, // 判斷值是否是響應式物件
  isReadonly, // 判斷值是否是隻讀物件
  isProxy, // 判斷值是否是 proxy
  shallowReactive, // 生成淺響應式物件
  shallowReadonly, // 生成淺只讀物件
  markRaw, // 讓資料不可被代理
  toRaw, // 獲取代理物件的原始物件
  ReactiveFlags, // 型別宣告
  DeepReadonly // 型別宣告
} from './reactive'
export {
  computed, // 計算屬性
  ComputedRef, // 型別宣告
  WritableComputedRef, // 型別宣告
  WritableComputedOptions, // 型別宣告
  ComputedGetter, // 型別宣告
  ComputedSetter // 型別宣告
} from './computed'
export {
  effect, // 定義副作用函式, 返回 effect 本身, 稱為 runner
  stop, // 停止 runner
  track, // 收集 effect 到 Vue3 內部的 targetMap 變數
  trigger, // 執行 targetMap 變數儲存的 effects
  enableTracking, // 開始依賴收集
  pauseTracking, // 停止依賴收集
  resetTracking, // 重置依賴收集狀態
  ITERATE_KEY, // 固定引數
  ReactiveEffect, // 型別宣告
  ReactiveEffectOptions, // 型別宣告
  DebuggerEvent // 型別宣告
} from './effect'
export {
  TrackOpTypes, // track 方法的 type 引數的列舉值
  TriggerOpTypes // trigger 方法的 type 引數的列舉值
} from './operations'

一、名詞解釋

  • target: 普通的 JS 物件

  • reactive: @vue/reactivity 提供的函式, 接收一個物件, 並返回一個 代理物件, 即響應式物件

  • shallowReactive: @vue/reactivity 提供的函式, 用來定義淺響應物件

  • readonly:@vue/reactivity 提供的函式, 用來定義只讀物件

  • shallowReadonly: @vue/reactivity 提供的函式, 用來定義淺只讀物件

  • handlers: Proxy 物件暴露的鉤子函式, 有 get()set()deleteProperty()ownKeys() 等, 可以參考MDN

  • targetMap: @vue/reactivity 內部變數, 儲存了所有依賴

  • effect: @vue/reactivit 提供的函式, 用於定義副作用, effect(fn, options) 的引數就是副作用函式

  • watchEffect: @vue/runtime-core 提供的函式, 基於 effect 實現

  • track: @vue/reactivity 內部函式, 用於收集依賴

  • trigger: @vue/reactivity 內部函式, 用於消費依賴

  • scheduler: effect 的排程器, 允許使用者自行實現

二、Vue3 實現響應式的思路

先看下邊的流程簡圖, 圖中 Vue 程式碼的功能是: 每隔一秒在 idBoxdiv 中輸出當前時間

在開始梳理 Vue3 實現響應式的步驟之前, 要先簡單理解 effect, effect 是響應式系統的核心, 而響應式系統又是 Vue3 的核心

上圖中從 tracktargetMap 的黃色箭頭, 和從 targetMaptrigger 的白色箭頭, 就是 effect 函式要處理的環節

effect 函式的語法為:

effect(fn, options)

effect 接收兩個引數, 第一個必填引數 fn 是副作用函式

第二個選填 options 的引數定義如下:

export interface ReactiveEffectOptions {
  lazy?: boolean                              // 是否延遲觸發 effect
  scheduler?: (job: ReactiveEffect) => void   // 排程函式
  onTrack?: (event: DebuggerEvent) => void    // 追蹤時觸發
  onTrigger?: (event: DebuggerEvent) => void  // 觸發回撥時觸發
  onStop?: () => void                         // 停止監聽時觸發
  allowRecurse?: boolean                      // 是否允許遞迴
}

下邊從流程圖中左上角的 Vue 程式碼開始

第 1 步

通過 reactive 方法將 target 物件轉為響應式物件, reactive 方法的實現方法如下:

import { mutableHandlers } from './baseHandlers'
import { mutableCollectionHandlers } from './collectionHandlers'

const reactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()

export function reactive(target: object) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const targetType = getTargetType(target) // 先忽略, 上邊例子中, targetType 的值為: 1
  const proxy = new Proxy(
    target,
    targetType === 2 ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

reactive 方法攜帶 target 物件和 mutableHandlersmutableCollectionHandlers 呼叫 createReactiveObject 方法, 這兩個 handers 先忽略

createReactiveObject 方法通過 reactiveMap 變數快取了一份響應式物件, reactiveMapreadonlyMap 變數是檔案內部的變數, 相當於檔案級別的閉包變數

其中 targetType 有三種列舉值: 0 代表不合法, 1 代表普通物件, 2 代表集合, 圖中例子中, targetType 的值為 1, 對於 { text: '' } 這個普通物件傳進 reactive() 方法時, 使用 baseHandlers 提供的 mutableHandlers

最後呼叫 Proxy 方法將 target 轉為響應式物件, 其中 "響應" 體現在 handers 裡, 可以這樣理解: reactive = Proxy (target, handlers)

第 2 步

mutableHandlers 負責掛載 getsetdeletePropertyhasownKeys 這五個方法到響應式物件上

其中 gethasownKeys 負責收集依賴, setdeleteProperty 負責消費依賴

響應式物件的 gethasownKeys 方法被觸發時, 會呼叫 createGetter 方法, createGetter 的實現如下:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

{ text: '' } 這個普通JS物件傳到 createGetter 時, key 的值為: text, res 的值為: String 型別, 如果 res 的值為 Object 型別則會遞迴呼叫, 將 res 轉為響應式物件

createGetter 方法的目的是觸發 track 方法, 對應本文的第 3 步

響應式物件的 setdeleteProperty 方法被觸發時, 會呼叫 createSetter 方法, createSetter 的實現如下:

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    return result
  }
}

createSetter 方法的目的是觸發 trigger 方法, 對應本文的第 4 步

第 3 步

這一步是整個響應式系統最關鍵的一步, 即我們常說的依賴收集, 依賴收集的概念很簡單, 就是把 響應式資料副作用函式 建立聯絡

文章一開始流程圖的例子中, 就是把 target 物件和 document.getElementById("Box").innerText = date.text; 這個副作用函式建立關聯, 這個 "關聯" 指的就是上邊提到的 targetMap 變數, 後邊會詳細描述一下 targetMap 物件的結構

第 2 步介紹了 createGetter 方法的核心是呼叫 track 方法, track 方法由 @/vue/reativity/src/effect.ts 提供, 下面看一下 track 的實現:

const targetMap = new WeakMap<any, KeyToDepMap>()

// target: { text: '' }
// type: get
// key: text
export function track(target: object, type: TrackOpTypes, key: unknown) {
  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)
  }
}

track 方法我們能看到 targetMap 這個閉包變數上儲存了所有的 effect, 換句話說是把能影響到 target 的副作用函式收集到 targetMap 變數中

targetMap 是個 WeakMap, WeakMap 和 Map 的區別在於 WeakMap 的鍵只能是物件, 用 WeakMap 而不用 Map 是因為 Proxy 物件不能代理普通資料型別

targetMap 的結構:

const targetMap = {
	[target]: {
		[key1]: [effect1, effect2, effect3, ...],
		[key2]: [effect1, effect2, effect3, ...]
	}
}

{ text: '' } 這個target 傳進來時, targetMap 的結構是:

// 上邊例子中用來在 id 為 Box 的 div 中輸出當前時間的副作用函式
const effect = () => {
	document.getElementById("Box").innerText = date.text;
};

const target = {
	"{ text: '' }": {
		"text": [effect]
	}
}

舉三個例子, 來分析一下 targetMap 的結構, 第一個例子是多個 target 情況:

<script>
import { effect, reactive } from "@vue/reactivity";

const target1 = { language: "JavaScript"};
const target2 = { language: "Go"};
const target3 = { language: "Python"};
const r1 = reactive(target1);
const r2 = reactive(target2);
const r3 = reactive(target3);

// effect1
effect(() => {
  console.log(r1.language);
});

// effect2
effect(() => {
  console.log(r2.language);
});

// effect3
effect(() => {
  console.log(r3.language);
});

// effect4
effect(() => {
  console.log(r1.language);
  console.log(r2.language);
  console.log(r3.language);
});
</script>

這種情況下 targetMap 的構成是:

const effect1 = () => {
  console.log(r1.language);
};
const effect2 = () => {
  console.log(r2.language);
};
const effect3 = () => {
  console.log(r3.language);
};
const effect4 = () => {
  console.log(r1.language);
  console.log(r2.language);
  console.log(r3.language);
};

const targetMap = {
	'{"language":"JavaScript"}': {
		"language": [effect1, effect4]
	},
  '{"language":"Go"}': {
    "language": [effect2, effect4]
  },
  '{"language":"Python"}': {
    "language": [effect3, effect4]
  }
}

第二個例子是單個 target 多個屬性時:

import { effect, reactive } from "@vue/reactivity";
const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"};
const user = reactive(target);

effect(() => {
  console.log(user.name);
  console.log(user.age);
  console.log(user.email);
});

這種情況下 targetMap 的構成是:

const effect = () => {
  console.log(user.name);
  console.log(user.age);
  console.log(user.email);
};

const targetMap = {
  '{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': {
    "name": [effect],
    "age": [effect],
    "email": [effect]
  }
}

第三個例子是多維物件時:

import { effect, reactive } from "@vue/reactivity";
const target = {
  name: "rmlzy",
  skills: {
    frontend: ["JS", "TS"],
    backend: ["Node", "Python", "Go"]
  }
};
const user = reactive(target);

// effect1
effect(() => {
  console.log(user.name);
});

// effect2
effect(() => {
  console.log(user.skills);
});

// effect3
effect(() => {
  console.log(user.skills.frontend);
});

// effect4
effect(() => {
  console.log(user.skills.frontend[0]);
});

這種情況下 targetMap 的構成是:

const effect1 = () => {
  console.log(user.name);
};
const effect2 = () => {
  console.log(user.skills);
};
const effect3 = () => {
  console.log(user.skills.frontend);
};
const effect4 = () => {
  console.log(user.skills.frontend[0]);
};

const targetMap = {
  '{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': {
    "name": [effect1],
    "skills": [effect2, effect3, effect4]
  },
  '{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': {
    "frontend": [effect3, effect4]
  }
}

第 4 步

第 3 步的目的是收集依賴, 這一步的目的是消費依賴

這裡要注意, 只有當 target 代理物件的 get 方法被觸發時, 才會真正執行 track, 換句話說, 沒有地方需要 get target 物件時, target 沒有依賴, 也就沒有收集依賴一說

下邊的例子中只是把 target 轉換為了響應式物件, 並沒有觸發依賴收集, targetMap 是空的

const target = {"text": ""};
const date = reactive(target);
effect(() => {
  date.text = new Date().toString();
});

第 2 步介紹了 createSetter 方法的核心是呼叫 trigger 方法, trigger 方法由 @/vue/reativity/src/effect.ts 提供, 下面看一下 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>()
	if (isMap(target)) {
    effects.add(depsMap.get(ITERATE_KEY))
	}
  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  effects.forEach(run)
}

trigger 的實現很簡單, 先把 target 相關的 effect 彙總到 effects 陣列中, 然後呼叫 effects.forEach(run) 執行所有的副作用函式

再回顧一下 effect 方法的定義: effect(fn, options), 其中 options 有個可選屬性叫 scheduler, 從上邊 run 函式也可以看到 scheduler 的作用是讓使用者自定義如何執行副作用函式

第 5 步

又回到了本文最開始講的 effect, effect 函式的實現如下:

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

effect 的核心是呼叫 createReactiveEffect 方法

可以看到 options.lazy 預設為 false 會直接執行 effect, 當設定為 true 時, 會返回 effect 由使用者手動觸發

createReactiveEffect 函式的實現如下:

const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

首先定義了 effect 是個普通的 function, 先看後邊 effect 函式掛載的屬性:

effect.id = uid++ // 自增ID, 每個 effect 唯一的ID
effect.allowRecurse = !!options.allowRecurse // 是否允許遞迴
effect._isEffect = true // 特殊標記
effect.active = true // 啟用狀態
effect.deps = [] // 依賴陣列
effect.raw = fn // 快取一份使用者傳入的副作用函式
effect.options = options // 快取一份使用者傳入的配置

isEffect 函式用來判斷值是否是 effect, 就是根據上邊 _isEffect 變數判斷的, isEffect 函式實現如下:

function isEffect(fn) {
  return fn && fn._isEffect === true;
}

再來看 effect 的核心邏輯:

cleanup(effect)
try {
  enableTracking()
  effectStack.push(effect)
  activeEffect = effect
  return fn()
} finally {
  effectStack.pop()
  resetTracking()
  activeEffect = effectStack[effectStack.length - 1]
}

effectStack 用陣列實現棧, activeEffect 是當前生效的 effect

先執行 cleanup(effect):

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

cleanup 的目的是清空 effect.deps, deps 是持有該 effect 的依賴陣列, deps 的結構如下

清除完依賴後, 開始重新收集依賴, 把當前 effect 追加到 effectStack, 將 activeEffect 設定為當前的 effect, 然後呼叫 fn 並且返回 fn() 的結果

第 4 步提過到: "只有當 target 代理物件的 get 方法被觸發時, 才會真正執行 track", 至此才是真正的觸發了 target代理物件的 get 方法, 執行了track 方法然後收集到了依賴

等到 fn 執行結束, finally 階段, 把當前的 effect 彈出, 恢復 effectStack 和 activeEffect, Vue3 整個響應式的流程到此結束

三、知識點

activeEffect 的作用

我的理解是為了暴露給 onTrack 方法, 來整體看一下 activeEffect 出現的地方:

let activeEffect;

function effect(fn, options = EMPTY_OBJ) {
  const effect = createReactiveEffect(fn, options);
  return effect;
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
  	// 省略部分程式碼 ...
    try {
      activeEffect = effect;
      return fn();
    }
    finally {
      activeEffect = effectStack[effectStack.length - 1];
    }
  };
  // 省略部分程式碼 ...
  return effect;
}

function track(target, type, key) {
  if (activeEffect === undefined) {
    return;
  }
  let dep = targetMap.get(target).get(key); // dep 是儲存 effect 的 Set 陣列
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
    if (activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      });
    }
  }
}
  1. fn 執行前, activeEffect 被賦值為當前 effect

  2. fn 執行時的依賴收集階段, 獲取 targetMap 中的 dep (儲存 effect 的 Set 陣列), 並暴露給 options.onTrack 介面

effect 和 stop

@vue/reactivity 提供了 stop 函式, effect 可以被 stop 函式終止

const obj = reactive({ foo: 0 });

const runner = effect(() => {
  console.log(obj.foo);
});

// effect 被執行一次, 輸出 0

// obj.foo 被賦值一次, effect 被執行一次, 輸出 1
obj.foo ++;

// 停止 effect
stop(runner);

// effect 不會被觸發, 無輸出
obj.foo ++;

watchEffect 和 effect

  1. watchEffect 來自 @vue/runtime-core, effect 來自 @vue/reactivity
  2. watchEffect 基於 effect 實現
  3. watchEffect 會維護與元件例項的關係, 如果元件被解除安裝, watchEffect 會被 stop, 而 effect 不會被 stop

watchEffect 和 invalidate

watchEffect 接收的副作用函式, 會攜帶一個 onInvalidate 的回撥函式作為引數, 這個回撥函式會在副作用無效時執行

watchEffect(async (onInvalidate) => {
  let valid = true;
  onInvalidate(() => {
    valid = false;
  });
  const data = await fetch(obj.foo);
  if (valid) {
    // 獲取到 data
  } else {
    // 丟棄
  }
});

ref

JS資料型別:

  • 基本型別: String、Number、Boolean、Null、Undefined、Symbol
  • 引用資料型別: Object、Array、Function

因為 Proxy 只能代理物件, reactive 函式的核心又是 Proxy, 所以 reactive 不能代理基本型別

對於基本型別需要用 ref 函式將基本型別轉為物件:

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

其中 __v_isRef 引數用來標誌當前值是 ref 型別, isRef 的實現如下:

export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}

這樣做有個缺點, 需要多取一層 .value:

const myRef = ref(0);
effect(() => {
  console.log(myRef.value);
});
myRef.value = 1;

這也是 Vue ref 語法糖提案的原因, 可以參考 如何評價 Vue 的 ref 語法糖提案?

reactive 和 shallowReactive

shallowReactive 用來定義淺響應資料, 深層次的物件值是非響應式的:

const target = {
  foo: {
    bar: 1
  }
};
const obj = shallowReactive(target);

effect(() => {
  console.log(obj.foo.bar);
});

obj.foo.bar = 2; // 無效, reactive 則有效
obj.foo = { bar: 2 }; // 有效

readonly 和 shallowReadonly

類似 shallowReactive, 深層次的物件值是可以被修改的

markRaw 和 toRaw

markRaw 的作用是讓資料不可被代理, 所有攜帶 __v_skip 屬性, 並且值為 true 的資料都會被跳過:

export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

toRaw 的作用是獲取代理物件的原始物件:

const obj = {};
const reactiveProxy = reactive(obj);
console.log(toRaw(reactiveProxy) === obj); // true

computed

const myRef = ref(0);
const myRefComputed = computed(() => {
  return myRef.value * 2;
});
effect(() => {
  console.log(myRef.value * 2);
});

myRef 值變化時, computed 會執行一次, effect 會執行一次

myRef 值未變化時, computed 不會執行, effect 依舊會執行


如果你有問題歡迎留言和我交流, 閱讀原文

相關文章