Vue3.0數據響應式原理

基於Vue3.0發佈在GitHub上的初版源碼(2019.10.05)整理html

預備知識

  • ES6 Proxy,整個響應式系統的基礎。
  • 新的composition-API的基本使用,目前尚未中文文檔,能夠先經過這個倉庫(composition-api-rfc)瞭解,裏面也有對應的在線文檔。

先把Vue3.0跑起來

先把vue-next倉庫的代碼clone下來,安裝依賴而後構建一下,vue的package下的dist目錄下找到構建的腳本,引入腳本便可。 下面一個簡單計數器的DEMO:vue

<!DOCTYPE html>
<html lang="en">
<body>
  <div id='app'></div>
</body>
<script src="./dist/vue.global.js"></script>
<script> const { createApp, reactive, computed } = Vue; const RootComponent = { template: ` <button @click="increment"> Count is: {{ state.count }} </button> `, setup() { const state = reactive({ count: 0, }) function increment() { state.count++ } return { state, increment } } } createApp().mount(RootComponent, '#app') </script>
</html>
複製代碼

template和以前同樣,一樣Vue3也支持手寫render的寫法,templaterender同時存在的狀況,優先renderreact

setup選項是新增的主要變更,顧名思義,setup函數會在組件掛載前(beforeCreatecreated生命週期之間)運行一次,相似組件初始化的做用,setup須要返回一個對象或者函數。返回對象會被賦值給組件實例的renderContext,在組件的模板做用域能夠被訪問到,相似data的返回值。返回函數會被當作是組件的render。具體能夠細看文檔。git

reactive的做用是將對象包裝成響應式對象,經過Proxy代理後的對象。github

上面的計數器的例子,在組件的setup函數中,建立了一個響應式對象state包含一個count屬性。而後建立了一個increment遞增的函數,最後將stateincrement返回給做用域,這樣template裏的button按鈕就能訪問到increment函數綁定到點擊的回調,count也能顯示在按鈕上。咱們點擊按鈕,按鈕上的數值就能跟着遞增。web

下面切入正題,咱們就來探究下按鈕上count值跟着響應式更新的原理api

數據結構

首先列一下主要的一些數據結構,先列在這裏,後面提到能夠翻回來看看。緩存

ReactiveEffect 一個Function對象,用於執行組件的掛載和更新。數據結構

interface ReactiveEffect {
  (): any
  isEffect: true
  active: boolean
  raw: Function // 具體執行的函數
  deps: Array<Dep>
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}
複製代碼

targetMap 相似 {target -> key -> dep}的一個Map結構,用於緩存全部響應式對象和依賴收集。app

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
複製代碼

Proxy代理攔截

reactive函數執行,會將傳入的target對象經過Proxy包裝,攔截它的getset等,並將代理的target緩存到targetMaptargetMap.set(target, new Map())

代理的get的時候會調用一個track函數,而set會調用一個triger函數。分別對應依賴收集和觸發更新。

// Proxy get 簡化
function get(target: any, key: string | symbol, receiver: any) {
  // 經過key拿到原始值res
  const res = Reflect.get(target, key, receiver)
  // 過濾不須要代理的狀況
  // ...
  // 依賴收集
  track(target, OperationTypes.GET, key)
  // 若是取到的值是個對象,將對象再代理包裝一下
  // Proxy只能代理對象第一層級
  return isObject(res) ? reactive(res) : res
}

// Proxy set 簡化
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
  // 一些不須要代理設置的場景
  // ...

  // 設置原始對象的值
  const result = Reflect.set(target, key, value, receiver)
  // 避免重複trigger的邏輯
  // ...
  // 觸發通知更新
  trigger(target, '更新的類型, 新增key或更新key', key)
  return result
}
複製代碼

依賴收集和觸發更新

組件在render階段,視圖會讀取數據對象上的值進行渲染,此時便觸發了Proxyget,由此觸發對應的track函數,記錄下了對應的ReactiveEffect,也就是常說的依賴收集。 ReactiveEffect其實就能夠看做是組件的更新(mount是特殊的update),數據的變動觸發triggertrigger遍歷調用track收集的對應的數據的ReactiveEffect,也就是對應有關聯的組件的更新。

trigger觸發的組件的更新,在render階段又觸發了新一輪的track依賴收集,更新依賴。

// 簡化的 track
function track( target: any, type: OperationTypes, key?: string | symbol ) {
  // 只有在依賴收集階段才進行依賴收集
  // 除了render,其餘場景也可能會觸發Proxy的get,但不須要進行依賴收集
  // activeReactiveEffectStack棧頂包裝了當前render的組件的mount和update的邏輯
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  // 若是effect爲空,說明當前不在render階段
  if (effect) {
    // ...
    // =====>初始化對應{target -> key -> dep}的結構
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key as string | symbol)
    if (!dep) {
      depsMap.set(key as string | symbol, (dep = new Set()))
    }
    // <=====初始化對應{target -> key -> dep}的結構
    // 依賴列表裏若是沒有,add
    if (!dep.has(effect)) {
      // 這裏將effect做爲依賴,緩存到依賴列表
      dep.add(effect)
      effect.deps.push(dep)
    }
  }
}

// 簡化的trigger
function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) {
  // 獲取對應target在track過程當中緩存的依賴
  const depsMap = targetMap.get(target)

  const effects: Set<ReactiveEffect> = new Set()
  // 省略分類邏輯
  depsMap.forEach(dep => {
    // 將effect分類過濾添加到effects
  })
  
  const run = (effect: ReactiveEffect) => {
    // 有個異步調度的過程,nextTick
    scheduleRun(effect, target, type, key, extraInfo)
  }

  effects.forEach(run)
}

複製代碼

大體流程:

vue3_reactive

總結

如今的代碼只有新特性的實現,並且ES6+TS的組合可讀性大大提升,編輯器支持也很好,因此相對會好讀不少。這裏只是簡單的理了一下vue 3.0 reactive的總體流程,細節還有不少地方值得學習,繼續加油。

/** PS:咱們團隊還缺人,有想換工做的小夥伴能夠發簡歷給我哦, base深圳,funkyfang(艾特)webank.com。 */

複製代碼
相關文章
相關標籤/搜索