Vue 3 響應式原理學習
開篇
我們以一段程式碼為例子:
<template> <img src="./logo.png"> <h1>Hello Vue 3!</h1> <button @click="add">Clicked {{ state.observe }} times.</button> </template> <script> import { ref } from 'vue' import {computed, reactive} from "@vue/reactivity"; import {onMounted, watchEffect} from"@vue/runtime-core"; export default { setup() { const state = reactive({ observe: 0, other: 2 }) const add = () => { state.observe++ } return { add, state, } } } </script> <style scoped> img { width: 200px; } h1 { font-family: Arial, Helvetica, sans-serif; }</style>
Effect安裝之前的流程
setupComoponet
這裡就會執行我們寫的setup函式,從原始碼來看。
這裡主要做了幾件事情:
-
執行setup函式,setup函式有我們的
reactive()
函式讓資料成為一個new Proxy。我們的傳入reactive函式的引數是這樣的,記住為這個引數物件設定了一個new Proxy代理!!const state = reactive({ observe: 0, other: 2 })
-
拿到setup函式返回的結果就是下面這個物件,並且使用new Proxy對他進行代理,然後儲存在instance中
{ add, state, }
setupRenderEffect函式
setupRenderEffect
函式建立一個effect為後面的響應式開啟之路作鋪墊。下面試這個函式的原始碼
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // create reactive effect for rendering instance.update = effect(function componentEffect() { ... }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }
函式的的第一個馬上執行effect函式,傳入的引數為一個 componentEffect的函式。第一個引數執行完之後是一個物件,物件的形式是這樣的:
{ scheduler: queueJob, allowRecurse: true, onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0, onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 }
下面來看下這個effect函式做了什麼事情,這個是effect函式的原始碼:
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } //這個effect是reactiveEffect這個函式 const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }
目前的步驟中不會執行到第一個if語句的內容,我們直接看第二句話const effect = createReactiveEffect(fn, options)
下面是這個createReactiveEffect的原始碼
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._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }
這裡做了幾件事情:
-
effect是要給叫做reactiveEffect()的函式
-
然後在effect上定義了十分多的屬性,其中有一個屬性叫做deps。我們馬上聯想到2.x的deps屬性
-
傳入引數fn,不要忘記這個引數fn是一個叫做
componentEffect()
函式 -
最終把這個effect返回出去
我們回到上面的effect函式。接下來執行的是下面這段程式碼,看看上面這個options。並滅有這個lazy屬性,那麼下面就執行執行剛剛返回的effect函式,effect本質上是reactiveEffect函式,下面看看reactiveEffect函式做了什麼事情
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { ... if (!options.lazy) { effect() } return effect } //effectStack是在本檔案上面定義的一個全域性變數。是一個數組 const effectStack: ReactiveEffect[] = [] 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屬性的時候effect.active
是被設定為false的,所以開始的時候不會執行if語句裡面的程式碼。
然後判斷effectStack裡面是否函式這個effect。effectStack是在本檔案上面定義的一個全域性變數。是一個數組,初始化是沒有任何東西的,那麼程式碼就會執行進入if語句裡面。
if語句第一句程式碼執行 cleanup(effect)
,cleanup() 的邏輯其實在Vue 2x
的原始碼中也有的,避免依賴的重複收集。
然後,執行enableTracking()
和effectStack.push(effect)
,前者的邏輯很簡單,即可以追蹤,用於後續觸發 track
的判斷,:
//trackStack也是本檔案定義的一個全域性陣列 const trackStack: boolean[] = [] function enableTracking() { trackStack.push(shouldTrack); shouldTrack = true; }
然後把這個effct push 進入這個effectStack.那麼此時effectStack就有一個元素,是一個effect,effect就是一個叫做reactiveEffect的函式,trackStack也有一個元素,是一個布林值,他們兩個分別是這樣的:
effect [ ƒ reactiveEffect() ] trackStack: [true]
做完這些工作之後把effect賦值給一個叫做activeEffect,最後執行fn,並且返回fn的執行結果。不要忘記fn是什麼了,fn是一個叫做componentEffect()
函式。
現在我們回頭看會最初的setupRenderEffect函式的程式碼看看這個componentEffect()
函式的作用。這個函式的的原始碼如下:
function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, parent } = instance ... const subTree = (instance.subTree = renderComponentRoot(instance)) ... } else { ... } }
我們只需要看第一個if語句塊裡面執行的結果,接下來進入元件的渲染階段,我們直接看到renderComponentRoot函式。
下面是這個函式的原始碼:
export function renderComponentRoot( instance: ComponentInternalInstance ): VNode { const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx } = instance let result currentRenderingInstance = instance if (__DEV__) { accessedAttrs = false } try { let fallthroughAttrs if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // withProxy is a proxy with a different `has` trap only for // runtime-compiled render functions using `with` block. const proxyToUse = withProxy || proxy result = normalizeVNode( //在這裡就真正觸發響應式依賴的收集和2.x類似 render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) fallthroughAttrs = attrs } ... return result }
開頭這個函式從instance上解構出十分多的屬性,instance是在前面的階段產生的一個物件。關鍵我們來看這段程式碼:
const proxyToUse = withProxy || proxy const proxyToUse = withProxy || proxy result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) )
withProxy 和 proxy都是從instance上解構出來的的。在我們這個例子中他們兩個分別是這樣的:withProxy是一個null,proxy是一個new Proxy代理
withProxy: null proxy: Proxy
那很顯然執行完const proxyToUse = withProxy || proxy
這個之後,const proxyToUse = proxy
。在本例子中它大概長這樣:
然後接下來執行:
result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) )
先執行render!.call
這段程式碼,這段程式碼就是真正開始進行響應式響應式收集的入口。
這裡會讀取這個我們在開始定義的響應式資料即(下面程式碼的)state.observe,原因在於我們在模板中使用了這個state.observe。
但是注意這裡其實會觸發兩個get。為什麼?原因在於在開始的時候setup整一個返回的物件即在本列子中(如下)這個物件也設定了一個new Proxy代理。所以我們在模板首先訪問的是state。會觸發一次get。然後在訪問state.observe.觸發第二次get。並且在第一個次get的時候會去校驗isRef。我們直接跳過第一個get看第二個get,即當我們訪問state.observe的時候出發的get函式。
{ add, state, }
在開頭的setupComoponet
函式中,我們已經把這個reactive執行並且為裡面的引數物件設定了一個new Proxy代理。
const state = reactive({ observe: 0, other: 2 })
那麼我們讀取這個state.observe就會掉入new Proxy的get函式裡面。我們現在來看看在reactive函式中為這個引數物件設定的get函式是長什麼樣子的。
function get(target: Target, key: string | symbol, receiver: object) { ... const targetIsArray = isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) { //Reflect.get保證了Proxy的原生預設行為 return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) const keyIsSymbol = isSymbol(key) if ( keyIsSymbol ? builtInSymbols.has(key as symbol) : key === `__proto__` || key === `__v_isRef` ) { 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 }
這個get函式有三個引數,第一個是target就是我們在reactive傳入的物件引數(如下),第二個引數key是就是我是使用的observe
{ observe: 0, other: 2 }
我省略了程式碼開頭的部分if語句內容,因為他們在此時不會被執行。
目前函式第一句話執行判斷target是不是要給Array,顯然不是返回結果是false,那麼自然不會走到下面的if語句。然後執行
const res = Reflect.get(target, key, receiver)
。
這裡就是讀取observe的值,那麼讀取後res的值為0.
讀取完值後,判斷下我們的key是不是Symbol。顯然返回false。然後進入到了收集依賴最重要的這句話
if (!isReadonly) { track(target, TrackOpTypes.GET, key) }
isReadonly在我的程式碼中會是false。所以會執行track函式。他三個引數,第一個引數就是我們reative的物件。第二個引數是一個TS的列舉型別,本質就是一個字串’get‘,第三個引數就是我們的key observe。下面看看track的執行內容。
//targetMap是本檔案裡面開頭的一個全域性變數,開始的時候沒有內容 const targetMap = new WeakMap<any, KeyToDepMap>() 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 }) } } }
直接跳過開頭的判定語句,來到這句話let depsMap = targetMap.get(target)
因為開始的時候這個targetMap是沒有東西的,然後它嘗試去獲取這個target物件對應的鍵值。顯示獲取不到,所以depsMap 是一個undefined。
然後下面的if語句就觸發了,它為這個targetMap set了一個值。鍵就是target物件,值是一個Map。順帶把這個Map賦值給了depsMap。
那現在targetMap就有東西了,如下:
targetMap 0: {Object => Map(0)} 鍵 Object就是 { observe: 0, other: 2 }
接下來執行這句話let dep = depsMap.get(key)
在上面depsMap已經得到了初始化,那麼這句話嘗試在這個depsMap Map中獲取observe這個鍵的值,由於剛開始的時候這個Map並沒有東西。所以dep也是一個undefined。
接下來就進入了if語句給這個dep和depsMap賦值
if (!dep) { depsMap.set(key, (dep = new Set())) } //執行完這句話後,depsMap長這樣 depsMap: Map(1) {"observe" => Set(0)} dep同時變成了一個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是什麼嗎。這個activeEffect是一個函式名字叫做reactiveEffect()
的函式。大家可以翻到上面去看看。
現在就把這個函式正式收錄進入這個dep中。並且將當前 dep
新增到 activeEffect
的 deps
陣列中。和2.x十分得相似。2.x程式碼是這樣的:
if (Dep.target) { dep.depend() ... }
那麼整個track的收集依賴的過程結束了。
最後get函式把訪問屬性的結果的值返回出去。就完成了這個get函式的過程。
上面介紹完怎麼收集依賴。後面的執行過程就不細講。總結一下整個get的流程(借用別人的一張圖片):
感覺其實和2.x的思想有相同的地方。只是一些細節表現上不同。
觸發set方法收集依賴的過程
假如我們嘗試點選按鈕,然這個state.observe進行++。現在就觸發了set方法
我看看set函式的程式碼:
function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) 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 }
第一句話就是重新獲取舊的值,然後執行toRaw方法。toRaw方法原始碼如下,先判斷你傳入的引數是否有東西。然後繼續呼叫toRaw,但是這次傳入的值是從observed['__v_raw']
但是顯然我們的value只是一個簡單的數字並沒有這個屬性。顯然就是直接返回這個值了。
那最後執行完這個toRaw方法後就是簡單把值返回一下
// export const enum ReactiveFlags { // SKIP = '__v_skip', // IS_REACTIVE = '__v_isReactive', // IS_READONLY = '__v_isReadonly', // RAW = '__v_raw' // } export function toRaw<T>(observed: T): T { return ( (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed ) }
然後執行這段程式碼:
const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) //顯然target不是一個數組。那麼就執行 hasOwn(target, key) //下面是hasOwn原始碼 const hasOwnProperty = Object.prototype.hasOwnProperty export const hasOwn = ( val: object, key: string | symbol ): key is keyof typeof val => hasOwnProperty.call(val, key) //在target中顯然有這個key屬性。所以這裡返回true //即hadKey==true
然後接下來繼續往下看set這段程式碼:
// 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
先進行一個判斷。根據剛剛的分析。這裡執行結果是true。上面看到hadKey的值為true。那麼就進入了else分支trigger(target, TriggerOpTypes.SET, key, value, oldValue)
trigger函式就是派發更新的方法,下面是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 (type === TriggerOpTypes.CLEAR) { ... } else if (key === 'length' && isArray(target)) { ... } 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: ... case TriggerOpTypes.DELETE: ... 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)
從上面的分析知道targetMap是這樣的:
那從Map中獲取這個object的鍵值。所以const depsMap = Map(1) {"observe" => Set(1)}
.
然後下面定義了一個set和一個add函式,暫且不看add函式,緊接著進入一系列的判斷語句,我們傳入的type引數是一個'set'字串並且我們的key不是'length'。所以最後進入了else分支,裁剪出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: ... case TriggerOpTypes.DELETE: ... //這個case條件等同於case 'set case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } }
我們的key不是undefined。所以進入if語句,if語句執行add方法,看看add原始碼:
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { //const effects = new Set<ReactiveEffect>() 在set開頭原始碼定義 if (effect !== activeEffect || effect.options.allowRecurse) { effects.add(effect) } }) } }
上面傳入的引數是depsMap.get(key),這個執行結果就是獲取到一個set(set的截圖如下)。set裡的內容就是function reactiveEffect()就是我們在get收集到的依賴。
//depsMap的樣子 0: {"observe" => Set(1)} //key就是observe
在add方法中,先判斷引數是否存在。存在的話就遍歷這個Set.並且新增到這個effects的Set中。
接下來進入switch語句,switch語句我們會進入最後那個:
switch (type) { case TriggerOpTypes.ADD: ... case TriggerOpTypes.DELETE: ... //這個case條件等同於case 'set case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break }
target顯示不是一個Map,是一個物件。直接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) }
最後定義了一個run方法。然後遍歷effects Set裡面的內容。逐個去執行run方法。看看run方法。
在執行run方法之前我們需要回顧一下上面定義的effect(非effects)是個什麼東西,因為這個effect就是run方法的引數,同時也是effects的Set裡面的元素,它長這樣的:
const effect = function reactiveEffect(): unknown { ... } as ReactiveEffect effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = { allowRecurse: true onTrack: undefined onTrigger: undefined scheduler: ƒ queueJob(job) }
根據它的長相,顯然進入第一個分支:
if (effect.options.scheduler) { effect.options.scheduler(effect) }
effect.options.scheduler是一個叫做queueJob的函式,看看它的原始碼
//本檔案開頭定義的一個全域性變數 const queue: (SchedulerJob | null)[] = [] export function queueJob(job: SchedulerJob) { if ( (!queue.length || !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex )) && job !== currentPreFlushParentJob ) { queue.push(job) queueFlush() } }
顯然這段程式碼先把effect推入這個數組裡面。在執行queueFlush函式,queueFlush函式如下:
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } }
到這裡其實和Vue 2.x已經很相似了。在 Vue 2x
中的 watcher
也是在下一個 tick
中執行,而 Vue 3.0
也是一樣。而 flushJobs
中就會對 queue
佇列中的 effect()
進行執行。後面就是最後執行準備渲染的邏輯。就不細說了。
把整個set整理為一個簡單的流程圖: