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

?最近學(xué)習(xí)了下Vue3的源碼,抽空寫一些自己對3.x源碼的解讀,同時算是學(xué)習(xí)的一個總結(jié)吧堤魁,也能加深自己的印象蜜暑。
?就先從3.x的響應(yīng)式系統(tǒng)說起吧铐姚。

回憶

?首先大概回憶一下2.x的響應(yīng)式系統(tǒng),主要由這幾個模塊組成肛捍,Observer隐绵,Watcher,Dep拙毫。
Observer負(fù)責(zé)通過defineProperty劫持Data依许,每個Data都各自在閉包中維護一個Dep的實例,用于收集依賴著它的Watcher恬偷。Dep維護一個公共的Target屬性悍手,用于保存當(dāng)前的需要被收集依賴的Watcher。每次Data被劫持的getter執(zhí)行的時候袍患,如果Dep.Target!==undefine, dep和Watcher實例就互相收集對方~
?2.x的響應(yīng)式系統(tǒng)其實是圍繞著Watcher坦康,也可以說圍繞著watch API的,包括render是一個renderWatcher诡延,computed是通過lazyWatcher實現(xiàn)滞欠。這并不是一個好的設(shè)計模式,不符合六個設(shè)計原則的(單一職責(zé)原則肆良,開閉原則)筛璧。而響應(yīng)式系統(tǒng)也無法獨立出來。

對比

?那么3.x是怎樣實現(xiàn)這一塊的內(nèi)容的呢惹恃。
?首先3.x響應(yīng)式系統(tǒng)相關(guān)的代碼在packages/reactivity/src里夭谤。3.x的響應(yīng)式系統(tǒng)的核心由兩個模塊構(gòu)成: effect, reactive。
?reactive模塊的功能比較簡單巫糙,就是給數(shù)據(jù)設(shè)置代理朗儒,類似于2.x的Observer,不同的點在于是用的Proxy去做代理参淹。
?effect模塊醉锄,傳入一個函數(shù),然后讓這個函數(shù)需要被響應(yīng)式數(shù)據(jù)影響浙值,目前具體在3.x中包括恳不,watch API,computed API开呐,還有組件的更新都是依賴effect實現(xiàn)的烟勋,但是這個模塊沒有暴露在Vue對象上面规求。所以說effect模塊是一個偏向于底層只有基礎(chǔ)功能的模塊,相比2.x神妹,這明顯是一個較好的設(shè)計模式颓哮。

Effect

?關(guān)于effect模塊,最主要的是里面的effect鸵荠,track冕茅,trigger三個方法。
?effect方法是一個高階函數(shù)蛹找,或者也可以說是工廠方法姨伤,接收一個函數(shù)作為參數(shù),返回一個effect實例方法庸疾,它使這個函數(shù)中的響應(yīng)式數(shù)據(jù)可追蹤到這個effect實例乍楚,如果有響應(yīng)式數(shù)據(jù)發(fā)生了改變,就會再次執(zhí)行這個effect届慈,可以參照源碼中調(diào)用這個方法的三個地方computed.ts,apiWatch.ts,renderer.ts徒溪。
?首先來看看track:以下是track方法的主要邏輯以及注釋,track方法按字面的解釋就是追蹤金顿,會在數(shù)據(jù)Proxy的get代理中調(diào)用臊泌,track這個數(shù)據(jù)本身。其實簡單說就做了一件事情揍拆,把當(dāng)前的active effect收集到響應(yīng)式數(shù)據(jù)的depsMap里面渠概。
其實并不復(fù)雜,這里和2.x不同的是嫂拴,2.x是每個數(shù)據(jù)各自都在閉包中維護deps對象播揪,這里是用一個全局的Store去保存響應(yīng)式數(shù)據(jù)影響的effects,實現(xiàn)了模塊的解耦筒狠。

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

?看完track方法的邏輯之后冯袍,effect方法的主要邏輯其實就呼之欲出了匈挖,那就是啟動響應(yīng)式追蹤---設(shè)置shouldTrack為true碾牌,設(shè)置activeEffect為當(dāng)前的effect,然后再調(diào)用傳入的方法并追蹤依賴儡循,最后返回一個封裝后的實例effect方法舶吗。

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

// createReactiveEffect是一個工廠方法誓琼,返回一個函數(shù)實例
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,跳過追蹤直接調(diào)用傳入的函數(shù)
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    // 清除effect中之前記錄的deps
    cleanup(effect)
    try {
      // 設(shè)置shouldTrack為true
      enableTracking()
      // 設(shè)置activeEffect為當(dāng)前的effect肴捉,另外把當(dāng)前的effect入棧(比如渲染子組件的時候腹侣,這個棧就起作用了)
      effectStack.push(effect)
      activeEffect = effect
      // 執(zhí)行傳入effect的函數(shù)
      return fn(...args)
    } finally {
      effectStack.pop()
      // 設(shè)置shouldTrack為上一次的shouldTrack(注:和effect一樣,shouldTrack也有一個棧)
      resetTracking()
      // 設(shè)置activeEffect為上一個activeEffect
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
}

?最后來看一下trigger方法齿穗,trigger方法的調(diào)用在Proxy的set代理中傲隶,作用就是在修改一個響應(yīng)式數(shù)據(jù)的時候,執(zhí)行這個響應(yīng)式對象的depsMap中所有的effect窃页。

// target為修改的響應(yīng)式數(shù)據(jù)對象跺株,type為操作類型,key為target上具體修改的參數(shù)
// 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,說明數(shù)據(jù)類型是Map胚嘲,或者Set(注意作儿,3.x的響應(yīng)式系統(tǒng)是支持Map和Set的)
  // CLEAR操作需要觸發(fā)集合上的所有屬性的effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(dep => {
      // addRunners功能其實很簡單,就是區(qū)分這個effect是普通的effect還是一個computed effect
      addRunners(effects, computedRunners, dep)
    })
  // 如果是更改length長度馋劈,說明是個數(shù)組攻锰,只需要觸發(fā)key在這個新的length之后的數(shù)據(jù)
  } 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) {
      // 大部分的情況,觸發(fā)這個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)
    ) {
      // 如果是添加/刪除數(shù)組里的項妓雾,或者Set娶吞,Map的add,delete械姻,set幾個方法妒蛇,同時也會改變length或者size,
      // 在Map和Set里面楷拳,受size影響的一些方法(比如size绣夺,forEach,entries欢揖,keys陶耍,values),都會把effect收集到ITERATE_KEY里面她混。
      // 具體可參考packages/reactivity/src/collectionHandler.ts里面的實現(xiàn)
      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功能其實很簡單烈钞,就是區(qū)分這個effect是普通的effect還是一個computed effect
// 普通的effect存在effects里面泊碑,computed effect存在computedRunners里面
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 調(diào)度將要執(zhí)行的effect,是否傳入effect.options.scheduler決定了執(zhí)行的方式
// 若沒有傳入毯欣,就立即同步執(zhí)行馒过,若有,則執(zhí)行調(diào)度方法酗钞,傳入effect
// 3.x中關(guān)于異步調(diào)度方法的實現(xiàn)可以查看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模塊相關(guān)的內(nèi)容就這些,下一篇是關(guān)于reactive模塊的算吩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末留凭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子偎巢,更是在濱河造成了極大的恐慌蔼夜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件压昼,死亡現(xiàn)場離奇詭異求冷,居然都是意外死亡,警方通過查閱死者的電腦和手機窍霞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門匠题,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人但金,你說我怎么就攤上這事韭山。” “怎么了冷溃?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵钱磅,是天一觀的道長。 經(jīng)常有香客問我似枕,道長盖淡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任凿歼,我火速辦了婚禮褪迟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘答憔。我一直安慰自己味赃,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布虐拓。 她就那樣靜靜地躺著心俗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侯嘀。 梳的紋絲不亂的頭發(fā)上另凌,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音戒幔,去河邊找鬼吠谢。 笑死,一個胖子當(dāng)著我的面吹牛诗茎,可吹牛的內(nèi)容都是我干的工坊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼敢订,長吁一口氣:“原來是場噩夢啊……” “哼王污!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起楚午,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤昭齐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后矾柜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阱驾,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年怪蔑,在試婚紗的時候發(fā)現(xiàn)自己被綠了里覆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡缆瓣,死狀恐怖喧枷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弓坞,我是刑警寧澤隧甚,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站昼丑,受9級特大地震影響呻逆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜菩帝,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一咖城、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呼奢,春花似錦宜雀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至禀综,卻和暖如春简烘,著一層夾襖步出監(jiān)牢的瞬間苔严,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工孤澎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留届氢,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓覆旭,卻偏偏與公主長得像退子,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子型将,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容