1. 程式人生 > 實用技巧 >Vue 3 響應式原理學習

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函式,從原始碼來看。

這裡主要做了幾件事情:

  1. 執行setup函式,setup函式有我們的reactive()函式讓資料成為一個new Proxy。我們的傳入reactive函式的引數是這樣的,記住為這個引數物件設定了一個new Proxy代理!!

     const state = reactive({
           observe: 0,
           other: 2
     })

  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
 }

  

這裡做了幾件事情:

  1. effect是要給叫做reactiveEffect()的函式

  2. 然後在effect上定義了十分多的屬性,其中有一個屬性叫做deps。我們馬上聯想到2.x的deps屬性

  3. 傳入引數fn,不要忘記這個引數fn是一個叫做componentEffect()函式

  4. 最終把這個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 新增到 activeEffectdeps 陣列中。和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整理為一個簡單的流程圖:

參考連結:https://segmentfault.com/a/1190000022198316