Vue3源碼--響應式原理1(effect)

?最近學習了下Vue3的源碼,抽空寫一些自己對3.x源碼的解讀,同時算是學習的一個總結吧,也能加深自己的印象。
?就先從3.x的響應式系統說起吧。

回憶

?首先大概回憶一下2.x的響應式系統,主要由這幾個模塊組成,Observer,Watcher,Dep。
Observer負責通過defineProperty劫持Data,每個Data都各自在閉包中維護一個Dep的實例,用于收集依賴著它的Watcher。Dep維護一個公共的Target屬性,用于保存當前的需要被收集依賴的Watcher。每次Data被劫持的getter執行的時候,如果Dep.Target!==undefine, dep和Watcher實例就互相收集對方~
?2.x的響應式系統其實是圍繞著Watcher,也可以說圍繞著watch API的,包括render是一個renderWatcher,computed是通過lazyWatcher實現。這并不是一個好的設計模式,不符合六個設計原則的(單一職責原則,開閉原則)。而響應式系統也無法獨立出來。

對比

?那么3.x是怎樣實現這一塊的內容的呢。
?首先3.x響應式系統相關的代碼在packages/reactivity/src里。3.x的響應式系統的核心由兩個模塊構成: effect, reactive。
?reactive模塊的功能比較簡單,就是給數據設置代理,類似于2.x的Observer,不同的點在于是用的Proxy去做代理。
?effect模塊,傳入一個函數,然后讓這個函數需要被響應式數據影響,目前具體在3.x中包括,watch API,computed API,還有組件的更新都是依賴effect實現的,但是這個模塊沒有暴露在Vue對象上面。所以說effect模塊是一個偏向于底層只有基礎功能的模塊,相比2.x,這明顯是一個較好的設計模式。

Effect

?關于effect模塊,最主要的是里面的effect,track,trigger三個方法。
?effect方法是一個高階函數,或者也可以說是工廠方法,接收一個函數作為參數,返回一個effect實例方法,它使這個函數中的響應式數據可追蹤到這個effect實例,如果有響應式數據發生了改變,就會再次執行這個effect,可以參照源碼中調用這個方法的三個地方computed.ts,apiWatch.ts,renderer.ts
?首先來看看track:以下是track方法的主要邏輯以及注釋,track方法按字面的解釋就是追蹤,會在數據Proxy的get代理中調用,track這個數據本身。其實簡單說就做了一件事情,把當前的active effect收集到響應式數據的depsMap里面。
其實并不復雜,這里和2.x不同的是,2.x是每個數據各自都在閉包中維護deps對象,這里是用一個全局的Store去保存響應式數據影響的effects,實現了模塊的解耦。

// target為傳入的響應式數據對象,type為操作類型,key為target上被追蹤的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果shouldTrack為false 或者 當前沒有活動中的effect,不需要執行追蹤的邏輯
  // shouldTrack為依賴追蹤提供一個全局的開關,可以很方便暫停/開啟,比如用于setup以及生命周期執行的時候
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 所有響應式數據都是被封裝的對象,所以用一個Map來保存更方便,Map的key為響應式數據的對象
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 同樣為每個響應式數據按key建立一個Set,用來保存target[key]所影響的effects
  let dep = depsMap.get(key)
  if (dep === void 0) {
    // 用一個Set去保存effects,省去了去重的判斷
    depsMap.set(key, (dep = new Set()))
  }
  // 如果target[key]下面沒有當前活動中的effect,就把這個effect加入到這個deps中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

?看完track方法的邏輯之后,effect方法的主要邏輯其實就呼之欲出了,那就是啟動響應式追蹤---設置shouldTrack為true,設置activeEffect為當前的effect,然后再調用傳入的方法并追蹤依賴,最后返回一個封裝后的實例effect方法。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // createReactiveEffect是一個工廠方法,返回一個函數實例
  const effect = createReactiveEffect(fn, options)
  // 如果不是lazy effect(lazy effect主要用于computed),立即執行這個effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

// createReactiveEffect是一個工廠方法,返回一個函數實例
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // 如果effect.active為false,跳過追蹤直接調用傳入的函數
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    // 清除effect中之前記錄的deps
    cleanup(effect)
    try {
      // 設置shouldTrack為true
      enableTracking()
      // 設置activeEffect為當前的effect,另外把當前的effect入棧(比如渲染子組件的時候,這個棧就起作用了)
      effectStack.push(effect)
      activeEffect = effect
      // 執行傳入effect的函數
      return fn(...args)
    } finally {
      effectStack.pop()
      // 設置shouldTrack為上一次的shouldTrack(注:和effect一樣,shouldTrack也有一個棧)
      resetTracking()
      // 設置activeEffect為上一個activeEffect
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
}

?最后來看一下trigger方法,trigger方法的調用在Proxy的set代理中,作用就是在修改一個響應式數據的時候,執行這個響應式對象的depsMap中所有的effect。

// target為修改的響應式數據對象,type為操作類型,key為target上具體修改的參數
// newValue,oldValue, oldTarget都很好理解
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 === void 0) {
    // never been tracked
    return
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 如果操作類型是CLEAR,說明數據類型是Map,或者Set(注意,3.x的響應式系統是支持Map和Set的)
  // CLEAR操作需要觸發集合上的所有屬性的effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(dep => {
      // addRunners功能其實很簡單,就是區分這個effect是普通的effect還是一個computed effect
      addRunners(effects, computedRunners, dep)
    })
  // 如果是更改length長度,說明是個數組,只需要觸發key在這個新的length之后的數據
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        addRunners(effects, computedRunners, dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 大部分的情況,觸發這個key下面的effets
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    if (
      type === TriggerOpTypes.ADD ||
      type === TriggerOpTypes.DELETE ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      // 如果是添加/刪除數組里的項,或者Set,Map的add,delete,set幾個方法,同時也會改變length或者size,
      // 在Map和Set里面,受size影響的一些方法(比如size,forEach,entries,keys,values),都會把effect收集到ITERATE_KEY里面。
      // 具體可參考packages/reactivity/src/collectionHandler.ts里面的實現
      const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => {
    scheduleRun(
      effect,
      target,
      type,
      key,
      __DEV__
        ? {
            newValue,
            oldValue,
            oldTarget
          }
        : undefined
    )
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // run每個effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

// addRunners功能其實很簡單,就是區分這個effect是普通的effect還是一個computed effect
// 普通的effect存在effects里面,computed effect存在computedRunners里面
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 調度將要執行的effect,是否傳入effect.options.scheduler決定了執行的方式
// 若沒有傳入,就立即同步執行,若有,則執行調度方法,傳入effect
// 3.x中關于異步調度方法的實現可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法
function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: TriggerOpTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

?以上源碼都是基于 vue-next-alpha8 版本。
?effect模塊相關的內容就這些,下一篇是關于reactive模塊的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容