vue3.0 源碼解析二 :數據綁定原理(下)

回顧上文

上節咱們講了數據綁定proxy原理,vue3.0用到的基本的攔截器,以及reactive入口等等。調用reactive創建響應式,首先經過判斷數據類型來肯定使用的hander,而後建立proxy代理對象observed。這裏的疑惑點就是hander對象具體作了什麼?本文咱們將已baseHandlers爲着手點,繼續分析響應式原理。html

連載文章是大體是這樣的,可能會根據變化隨時更改:
1 數據綁定原理(上)
2 數據綁定原理(下)
3 computed和watch原理
4 事件系統
5 ceateApp
6 初始化mounted和patch流程。
7 diff算法與2.0區別
8 編譯compiler系列
...vue

一 攔截器對象baseHandlers -> mutableHandlers

以前咱們介紹過baseHandlers就是調用reactive方法createReactiveObject傳進來的mutableHandlers對象。
咱們先來看一下mutableHandlers對象node

mutableHandlersreact

攔截器的做用域

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

vue3.0 用到了以上幾個攔截器,咱們在上節已經介紹了這幾個攔截器的基本用法,首先咱們對幾個基本用到的攔截器在作一下回顧。算法

①get,對數據的讀取屬性進行攔截,包括 target.點語法 和 target[]api

②set,對數據的存入屬性進行攔截 。數組

③deleteProperty delete操做符進行攔截。微信

vue2.0不能對對象的delete操做符進行屬性攔截。app

例子🌰:函數

delete object.a

是沒法監測到的。

vue3.0proxy中deleteProperty 能夠攔截 delete 操做符,這就表述vue3.0響應式能夠監聽到屬性的刪除操做。

④has,對 in 操做符進行屬性攔截。

vue2.0不能對對象的in操做符進行屬性攔截。

例子

a in object

has 是爲了解決如上問題。這就表示了vue3.0能夠對 in 操做符 進行攔截。

⑤ownKeys Object.keys(proxy) ,for...in...循環 Object.getOwnPropertySymbols(proxy)Object.getOwnPropertyNames(proxy) 攔截器

例子

Object.keys(object)

說明vue3.0能夠對以上這些方法進行攔截。

二 組件初始化階段

若是咱們想要弄明白整個響應式原理。那麼組件初始化,到初始化過程當中composition-api的reactive處理data,以及編譯階段對data屬性進行依賴收集是分不開的。vue3.0提供了一套從初始化,到render過程當中依賴收集,到組件更新,到組件銷燬完整響應式體系,咱們很難從一個角度把東西講明白,因此在正式講攔截器對象如何收集依賴,派發更新以前,咱們看看effect作了些什麼操做。

1 effect -> 新的渲染watcher

vue3.0用effect反作用鉤子來代替vue2.0watcher。咱們都知道在vue2.0中,有渲染watcher專門負責數據變化後的重新渲染視圖。vue3.0改用effect來代替watcher達到一樣的效果。

咱們先簡單介紹一下mountComponent流程,後面的文章會詳細介紹mount階段的

1 mountComponent 初始化mountComponent

// 初始化組件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 建立component 實例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步 : TODO:初始化 初始化組件,創建proxy , 根據字符竄模版獲得 */
    setupComponent(instance)
    /* 第三步:創建一個渲染effect,執行effect */
    setupRenderEffect(
      instance,     // 組件實例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }

上面是整個mountComponent的主要分爲了三步,咱們這裏分別介紹一下每一個步驟幹了什麼:
① 第一步: 建立component 實例 。
② 第二步:初始化組件,創建proxy ,根據字符竄模版獲得render函數。生命週期鉤子函數處理等等
③ 第三步:創建一個渲染effect,執行effect。

從如上方法中咱們能夠看到,在setupComponent已經構建了響應式對象,可是尚未初始化收集依賴

2 setupRenderEffect 構建渲染effect

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 建立一個渲染 effect */
    instance.update = effect(function componentEffect() {
      //...省去的內容後面會講到
    },{ scheduler: queueJob })
  }

爲了讓你們更清楚的明白響應式原理,我這隻保留了和響應式原理有關係的部分代碼。

setupRenderEffect的做用

① 建立一個effect,並把它賦值給組件實例的update方法,做爲渲染更新視圖用。
② componentEffect做爲回調函數形式傳遞給effect做爲第一個參數

3 effect作了些什麼

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  /* 若是不是懶加載 當即執行 effect函數 */
  if (!options.lazy) {
    effect()
  }
  return effect
}

effect做用以下

① 首先調用。createReactiveEffect
② 若是不是懶加載 當即執行 由createReactiveEffect建立出來的ReactiveEffect函數

4 ReactiveEffect

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T, /**回調函數 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {
        enableTracking()
        effectStack.push(effect) //往effect數組中裏放入當前 effect
        activeEffect = effect //TODO: effect 賦值給當前的 activeEffect
        return fn(...args) //TODO:    fn 爲effect傳進來 componentEffect
      } finally {
        effectStack.pop() //完成依賴收集後從effect數組刪掉這個 effect
        resetTracking() 
        /* 將activeEffect還原到以前的effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化參數 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO:用於收集相關依賴 */
  effect.options = options
  return effect
}

createReactiveEffect

createReactiveEffect的做用主要是配置了一些初始化的參數,而後包裝了以前傳進來的fn,重要的一點是把當前的effect賦值給了activeEffect,這一點很是重要,和收集依賴有着直接的關係

在這裏留下了一個疑點,

①爲何要用effectStack數組來存放這裏effect

總結

咱們這裏個響應式初始化階段進行總結

① setupComponent建立組件,調用composition-api,處理options(構建響應式)獲得Observer對象。

② 建立一個渲染effect,裏面包裝了真正的渲染方法componentEffect,添加一些effect初始化屬性。

③ 而後當即執行effect,而後將當前渲染effect賦值給activeEffect

最後咱們用一張圖來解釋一下整個流程。

809B8CEF-B6CF-469E-92AB-A8B187D3C012.jpg

三 依賴收集,get作了些什麼?

1 迴歸mutableHandlers中的get方法

1 不一樣類型的get

/* 深度get */
const get = /*#__PURE__*/ createGetter()
/* 淺get */
const shallowGet = /*#__PURE__*/ createGetter(false, true)
/* 只讀的get */
const readonlyGet = /*#__PURE__*/ createGetter(true)
/* 只讀的淺get */
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

上面咱們能夠知道,對於以前講的四種不一樣的創建響應式方法,對應了四種不一樣的get,下面是一一對應關係。

reactive ---------> get

shallowReactive --------> shallowGet

readonly ----------> readonlyGet

shallowReadonly ---------------> shallowReadonlyGet

四種方法都是調用了createGetter方法,只不過是參數的配置不一樣,咱們這裏那第一個get方法作參考,接下來探索一下createGetter。

createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)
    /* 淺邏輯 */
    if (shallow) {
      !isReadonly && track(target, TrackOpTypes.GET, key)
      return res
    }
    /* 數據綁定 */
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ?
          /* 只讀屬性 */
          readonly(res)
          /*  */
        : reactive(res)
      : res
  }
}

這就是createGetter主要流程,特殊的數據類型ref咱們暫時先不考慮。
這裏用了一些流程判斷,咱們用流程圖來講明一下這個函數主要作了什麼?

774EE03C-E5D3-4CD5-9890-06BA4EB85C1F.jpg

咱們能夠得出結論:
在vue2.0的時候。響應式是在初始化的時候就深層次遞歸處理了
可是

與vue2.0不一樣的是,即使是深度響應式咱們也只能在獲取上一級get以後才能觸發下一級的深度響應式。
好比

setup(){
 const state = reactive({ a:{ b:{} } })
 return {
     state
 }
}

在初始化的時候,只有a的一層級創建了響應式,b並無創建響應式,而當咱們用state.a的時候,纔會真正的將b也作響應式處理,也就是說咱們訪問了上一級屬性後,下一代屬性纔會真正意義上創建響應式

這樣作好處是,
1 初始化的時候不用遞歸去處理對象,形成了沒必要要的性能開銷。
*2 有一些沒有用上的state,這裏就不須要在深層次響應式處理。

2 track->依賴收集器

咱們先來看看track源碼:

track作了些什麼

/* target 對象自己 ,key屬性值  type 爲 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  /* 當打印或者獲取屬性的時候 console.log(this.a) 是沒有activeEffect的 當前返回值爲0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep觀察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 當前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每一個 activeEffect的deps 存放當前的dep */
    activeEffect.deps.push(dep)
  }
}

裏面主要引入了兩個概念 targetMapdepsMap

targetMap
鍵值對 proxy : depsMap
proxy : 爲reactive代理後的 Observer對象 。
depsMap :爲存放依賴dep的 map 映射。

depsMap
鍵值對:key : deps
key 爲當前get訪問的屬性名,
deps 存放effect的set數據類型。

咱們知道track做用大體是,首先根據 proxy對象,獲取存放deps的depsMap,而後經過訪問的屬性名key獲取對應的dep,而後將當前激活的effect存入當前dep收集依賴。

主要做用
①找到與當前proxy 和 key對應的dep。
②dep與當前activeEffect創建聯繫,收集依賴。

爲了方便理解,targetMapdepsMap的關係,下面咱們用一個例子來講明:
例子:
父組件A

<div id="app" >
  <span>{{ state.a }}</span>
  <span>{{ state.b }}</span>
<div>
<script>
const { createApp, reactive } = Vue

/* 子組件 */
const Children ={
    template="<div> <span>{{ state.c }}</span> </div>",
    setup(){
       const state = reactive({
          c:1
       })
       return {
           state
       }
    }
}
/* 父組件 */
createApp({
   component:{
       Children
   } 
   setup(){
       const state = reactive({
           a:1,
           b:2
       })
       return {
           state
       }
   }
})mount('#app')

</script>

咱們用一幅圖表示如上關係:

6143CE8B-B462-45DA-8E61-3DED09743E69.jpg

渲染effect函數如何觸發get

咱們在前面說過,建立一個渲染renderEffect,而後把賦值給activeEffect,最後執行renderEffect ,在這個期間是怎麼作依賴收集的呢,讓咱們一塊兒來看看,update函數中作了什麼,咱們回到以前講的componentEffect邏輯上來

function componentEffect() {
    if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, a, parent } = instance
        /* TODO: 觸發instance.render函數,造成樹結構 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          //觸發 beforeMount聲明週期鉤子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 觸發聲明週期 mounted鉤子 */
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新組件邏輯
        // ......
      }
}

這邊代碼大體首先會經過renderComponentRoot方法造成樹結構,這裏要注意的是,咱們在最初mountComponent的setupComponent方法中,已經經過編譯方法compile編譯了template模版的內容,state.a state.b等抽象語法樹,最終返回的render函數在這個階段會被觸發,在render函數中在模版中的表達式 state.a state.b 點語法會被替換成data中真實的屬性,這時候就進行了真正的依賴收集,觸發了get方法。接下來就是觸發生命週期 beforeMount ,而後對整個樹結構從新patch,patch完畢後,調用mounted鉤子

依賴收集流程總結

① 首先執行renderEffect ,賦值給activeEffect ,調用renderComponentRoot方法,而後觸發render函數。

② 根據render函數,解析通過compile,語法樹處理事後的模版表達式,訪問真實的data屬性,觸發get。

③ get方法首先通過以前不一樣的reactive,經過track方法進行依賴收集。

④ track方法經過當前proxy對象target,和訪問的屬性名key來找到對應的dep。

⑤ 將dep與當前的activeEffect創建起聯繫。將activeEffect壓入dep數組中,(此時的dep中已經含有當前組件的渲染effect,這就是響應式的根本緣由)若是咱們觸發set,就能在數組中找到對應的effect,依次執行。

最後咱們用一個流程圖來表達一下依賴收集的流程。

51E87C48-0C18-4F76-9F0C-3551C69080BC.jpg

四 set 派發更新

接下來咱們set部分邏輯。

const set = /*#__PURE__*/ createSetter()
/* 淺邏輯 */
const shallowSet = /*#__PURE__*/ createSetter(true)

set也是分兩個邏輯,set和shallowSet,兩種方法都是由createSetter產生,咱們這裏主要以set進行剖析。

createSetter建立set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    /* shallowSet邏輯 */

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    /* 判斷當前對象,和存在reactiveToRaw 裏面是否相等 */
    if (target === toRaw(receiver)) {
      if (!hadKey) { /* 新建屬性 */
        /*  TriggerOpTypes.ADD -> add */
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        /* 改變原有屬性 */
        /*  TriggerOpTypes.SET -> set */
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

createSetter的流程大體是這樣的

① 首先經過toRaw判斷當前的proxy對象和創建響應式存入reactiveToRaw的proxy對象是否相等。
② 判斷target有沒有當前key,若是存在的話,改變屬性,執行trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 若是當前key不存在,說明是賦值新屬性,執行trigger(target, TriggerOpTypes.ADD, key, value)。

trigger

/* 根據value值的改變,從effect和computer拿出對應的callback ,而後依次執行 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  /* 獲取depssMap */
  const depsMap = targetMap.get(target)
  /* 沒有通過依賴收集的 ,直接返回 */
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>()        /* effect鉤子隊列 */
  const computedRunners = new Set<ReactiveEffect>() /* 計算屬性隊列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) { /* 處理computed邏輯 */
            computedRunners.add(effect)  /* 儲存對應的dep */
          } else {
            effects.add(effect)  /* 儲存對應的dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) { /* 放進 scheduler 調度*/
      effect.options.scheduler(effect)
    } else {
      effect() /* 不存在調度狀況,直接執行effect */
    }
  }

  //TODO: 必須首先運行計算屬性的更新,以便計算的getter
  //在任何依賴於它們的正常更新effect運行以前,均可能失效。

  computedRunners.forEach(run) /* 依次執行computedRunners 回調*/
  effects.forEach(run) /* 依次執行 effect 回調( TODO: 裏面包括渲染effect )*/
}

咱們這裏保留了trigger的核心邏輯

① 首先從targetMap中,根據當前proxy找到與之對應的depsMap。
② 根據key找到depsMap中對應的deps,而後經過add方法分離出對應的effect回調函數和computed回調函數。
③ 依次執行computedRunners 和 effects 隊列裏面的回調函數,若是發現須要調度處理,放進scheduler事件調度

值得注意的的是:

此時的effect隊列中有咱們上述負責渲染的renderEffect,還有經過effectAPI創建的effect,以及經過watch造成的effect。咱們這裏只考慮到渲染effect。至於後面的狀況會在接下來的文章中和你們一塊兒分享。

咱們用一幅流程圖說明一下set過程。

F8EC8C48-445F-412E-A712-123667075195.jpg

五 總結

咱們總結一下整個數據綁定創建響應式大體分爲三個階段

1 初始化階段: 初始化階段經過組件初始化方法造成對應的proxy對象,而後造成一個負責渲染的effect。

2 get依賴收集階段:經過解析template,替換真實data屬性,來觸發get,而後經過stack方法,經過proxy對象和key造成對應的deps,將負責渲染的effect存入deps。(這個過程還有其餘的effect,好比watchEffect存入deps中 )。

3 set派發更新階段:當咱們 this[key] = value 改變屬性的時候,首先經過trigger方法,經過proxy對象和key找到對應的deps,而後給deps分類分紅computedRunners和effect,而後依次執行,若是須要調度的,直接放入調度。

還有一些問題沒有解決,好比:

① 爲何要用effectStack數組來存放這裏effect。
② 何時向deps存入其餘的effect。
等等...

帶着這些問題,但願咱們在接下來的文章中,一塊兒探討。

微信掃碼關注公衆號,按期分享技術文章

相關文章
相關標籤/搜索