Vue3 源碼之 reactivity

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

vue-next/reactivity 實現了 Vue3 的響應性, reactivity 提供瞭如下接口:html

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'

1、名詞解釋

  • target: 普通的 JS 對象vue

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

  • shallowReactive: @vue/reactivity 提供的函數, 用來定義淺響應對象python

  • readonly:@vue/reactivity 提供的函數, 用來定義只讀對象react

  • shallowReadonly: @vue/reactivity 提供的函數, 用來定義淺只讀對象git

  • handlers: Proxy 對象暴露的鉤子函數, 有 get()set()deleteProperty()ownKeys() 等, 能夠參考MDNgithub

  • targetMap: @vue/reactivity 內部變量, 存儲了全部依賴數組

  • effect: @vue/reactivit 提供的函數, 用於定義反作用, effect(fn, options) 的參數就是反作用函數緩存

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

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

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

  • scheduler: effect 的調度器, 容許用戶自行實現

2、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 整個響應式的流程到此結束

3、知識點

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 依舊會執行


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

相關文章
相關標籤/搜索