亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入了解Vue3中偵聽器watcher的實(shí)現(xiàn)原理

 更新時間:2022年04月14日 10:44:33   作者:風(fēng)度前端  
在平時的開發(fā)工作中,我們經(jīng)常使用偵聽器幫助我們?nèi)ビ^察某個數(shù)據(jù)的變化然后去執(zhí)行一段邏輯。在?Vue.js?2.x?中,你可以通過?watch?選項去初始化一個偵聽器,稱作?watcher。本文將詳細(xì)為大家介紹一下偵聽器的實(shí)現(xiàn)原理,需要的可以參考一下

在平時的開發(fā)工作中,我們經(jīng)常使用偵聽器幫助我們?nèi)ビ^察某個數(shù)據(jù)的變化然后去執(zhí)行一段邏輯。

在 Vue.js 2.x 中,你可以通過 watch 選項去初始化一個偵聽器,稱作 watcher:

export default { 
  watch: { 
    a(newVal, oldVal) { 
      console.log('new: %s,00 old: %s', newVal, oldVal) 
    } 
  } 
} 

當(dāng)然你也可以通過 $watch API 去創(chuàng)建一個偵聽器:

const unwatch = vm.$watch('a', function(newVal, oldVal) { 
  console.log('new: %s, old: %s', newVal, oldVal) 
}) 

與 watch 選項不同,通過 $watch API 創(chuàng)建的偵聽器 watcher 會返回一個 unwatch 函數(shù),你可以隨時執(zhí)行它來停止這個 watcher 對數(shù)據(jù)的偵聽,而對于 watch 選項創(chuàng)建的偵聽器,它會隨著組件的銷毀而停止對數(shù)據(jù)的偵聽。

在 Vue.js 3.0 中,雖然你仍可以使用 watch 選項,但針對 Composition API,Vue.js 3.0 提供了 watch API 來實(shí)現(xiàn)偵聽器的效果。本文就來分析下 watch API 的實(shí)現(xiàn)原理

watch API 的用法

我們先來看 Vue.js 3.0 中 watch API 有哪些用法。

1.watch API 可以偵聽一個 getter 函數(shù),但是它必須返回一個響應(yīng)式對象,當(dāng)該響應(yīng)式對象更新后,會執(zhí)行對應(yīng)的回調(diào)函數(shù)。

import { reactive, watch } from 'vue' 
const state = reactive({ count: 0 }) 
watch(() => state.count, (count, prevCount) => { 
  // 當(dāng) state.count 更新,會觸發(fā)此回調(diào)函數(shù) 
}) 

2.watch API 也可以直接偵聽一個響應(yīng)式對象,當(dāng)響應(yīng)式對象更新后,會執(zhí)行對應(yīng)的回調(diào)函數(shù)。

import { ref, watch } from 'vue' 
const count = ref(0) 
watch(count, (count, prevCount) => { 
  // 當(dāng) count.value 更新,會觸發(fā)此回調(diào)函數(shù) 
}) 

3.watch API 還可以直接偵聽多個響應(yīng)式對象,任意一個響應(yīng)式對象更新后,就會執(zhí)行對應(yīng)的回調(diào)函數(shù)。

import { ref, watch } from 'vue' 
const count = ref(0) 
const count2 = ref(1) 
watch([count, count2], ([count, count2], [prevCount, prevCount2]) => { 
  // 當(dāng) count.value 或者 count2.value 更新,會觸發(fā)此回調(diào)函數(shù) 
}) 

watch API實(shí)現(xiàn)原理

偵聽器的言下之意就是,當(dāng)偵聽的對象或者函數(shù)發(fā)生了變化則自動執(zhí)行某個回調(diào)函數(shù),這和副作用函數(shù) effect 很像, 那它的內(nèi)部實(shí)現(xiàn)是不是依賴了 effect 呢?帶著這個疑問,我們來探究 watch API 的具體實(shí)現(xiàn):

function watch(source, cb, options) { 
  if ((process.env.NODE_ENV !== 'production') && !isFunction(cb)) { 
    warn(``watch(fn, options?)` signature has been moved to a separate API. ` + 
      `Use `watchEffect(fn, options?)` instead. `watch` now only ` + 
      `supports `watch(source, cb, options?) signature.`) 
  } 
  return doWatch(source, cb, options) 
} 
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { 
  // 標(biāo)準(zhǔn)化 source 
  // 構(gòu)造 applyCb 回調(diào)函數(shù) 
  // 創(chuàng)建 scheduler 時序執(zhí)行函數(shù) 
  // 創(chuàng)建 effect 副作用函數(shù) 
  // 返回偵聽器銷毀函數(shù) 
}    

從代碼中可以看到,watch 函數(shù)內(nèi)部調(diào)用了 doWatch 函數(shù),調(diào)用前會在非生產(chǎn)環(huán)境下判斷第二個參數(shù) cb 是不是一個函數(shù),如果不是則會報警告以告訴用戶應(yīng)該使用 watchEffect(fn, options) API,watchEffect API 也是偵聽器相關(guān)的 API,稍后我們會詳細(xì)介紹。

下面我們就看看doWatch函數(shù)做了哪些事情

標(biāo)準(zhǔn)化source

我們先來看watch 函數(shù)的第一個參數(shù) source。

通過前文知道 source 可以是 getter 函數(shù),也可以是響應(yīng)式對象甚至是響應(yīng)式對象數(shù)組,所以我們需要標(biāo)準(zhǔn)化 source,這是標(biāo)準(zhǔn)化 source 的流程:

// source 不合法的時候會報警告 
const warnInvalidSource = (s) => { 
  warn(`Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + 
    `a reactive object, or an array of these types.`) 
} 
// 當(dāng)前組件實(shí)例 
const instance = currentInstance 
let getter 
if (isArray(source)) { 
  getter = () => source.map(s => { 
    if (isRef(s)) { 
      return s.value 
    } 
    else if (isReactive(s)) { 
      return traverse(s) 
    } 
    else if (isFunction(s)) { 
      return callWithErrorHandling(s, instance, 2 /* WATCH_GETTER */) 
    } 
    else { 
      (process.env.NODE_ENV !== 'production') && warnInvalidSource(s) 
    } 
  }) 
} 
else if (isRef(source)) { 
  getter = () => source.value 
} 
else if (isReactive(source)) { 
  getter = () => source 
  deep = true 
} 
else if (isFunction(source)) { 
  if (cb) { 
    // getter with cb 
    getter = () => callWithErrorHandling(source, instance, 2 /* WATCH_GETTER */) 
  } 
  else { 
    // watchEffect 的邏輯 
  } 
} 
else { 
  getter = NOOP 
  (process.env.NODE_ENV !== 'production') && warnInvalidSource(source) 
} 
if (cb && deep) { 
  const baseGetter = getter 
  getter = () => traverse(baseGetter()) 
} 

其實(shí),source 標(biāo)準(zhǔn)化主要是根據(jù) source 的類型,將其變成 getter 函數(shù)。具體來說:

  • 如果 source 是 ref 對象,則創(chuàng)建一個訪問 source.value 的 getter 函數(shù);
  • 如果 source 是 reactive 對象,則創(chuàng)建一個訪問 source 的 getter 函數(shù),并設(shè)置 deep 為 true(deep 的作用我稍后會說)
  • 如果 source 是一個函數(shù),則會進(jìn)一步判斷第二個參數(shù) cb 是否存在,對于 watch API 來說,cb 是一定存在且是一個回調(diào)函數(shù),這種情況下,getter 就是一個簡單的對 source 函數(shù)封裝的函數(shù)。

如果 source 不滿足上述條件,則在非生產(chǎn)環(huán)境下報警告,提示 source 類型不合法。

我們來看一下最終標(biāo)準(zhǔn)化生成的 getter 函數(shù),它會返回一個響應(yīng)式對象,在后續(xù)創(chuàng)建 effect runner 副作用函數(shù)需要用到,每次執(zhí)行 runner 就會把 getter 函數(shù)返回的響應(yīng)式對象作為 watcher 求值的結(jié)果,effect runner 的創(chuàng)建流程我們后續(xù)會詳細(xì)分析,這里不需要深入了解。

最后我們來關(guān)注一下 deep 為 true 的情況。此時,我們會發(fā)現(xiàn)生成的 getter 函數(shù)會被 traverse 函數(shù)包裝一層。traverse 函數(shù)的實(shí)現(xiàn)很簡單,即通過遞歸的方式訪問 value 的每一個子屬性。那么,為什么要遞歸訪問每一個子屬性呢?

其實(shí) deep 屬于 watcher 的一個配置選項,Vue.js 2.x 也支持,表面含義是深度偵聽,實(shí)際上是通過遍歷對象的每一個子屬性來實(shí)現(xiàn)。舉個例子你就明白了:

import { reactive, watch } from 'vue' 
const state = reactive({ 
  count: { 
    a: { 
      b: 1 
    } 
  } 
}) 
watch(state.count, (count, prevCount) => { 
  console.log(count) 
}) 
state.count.a.b = 2  

這里,我們利用 reactive API 創(chuàng)建了一個嵌套層級較深的響應(yīng)式對象 state,然后再調(diào)用 watch API 偵聽 state.count 的變化。接下來我們修改內(nèi)部屬性 state.count.a.b 的值,你會發(fā)現(xiàn) watcher 的回調(diào)函數(shù)執(zhí)行了,為什么會執(zhí)行呢?

原則上Proxy實(shí)現(xiàn)的響應(yīng)式對象,只有對象屬性先被訪問觸發(fā)了依賴收集,再去修改這個屬性,才可以通知對應(yīng)的依賴更新。而從上述業(yè)務(wù)代碼來看,我們修改 state.count.a.b 的值時并沒有訪問它 ,但還是觸發(fā)了 watcher 的回調(diào)函數(shù)。

根本原因是,當(dāng)我們執(zhí)行 watch 函數(shù)的時候,我們知道如果偵聽的是一個 reactive 對象,那么內(nèi)部會設(shè)置 deep 為 true, 然后執(zhí)行 traverse 去遞歸訪問對象深層子屬性,這個時候就會訪問 state.count.a.b 觸發(fā)依賴收集,這里收集的依賴是 watcher 內(nèi)部創(chuàng)建的 effect runner。因此,當(dāng)我們再去修改 state.count.a.b 的時候,就會通知這個 effect ,所以最終會執(zhí)行 watcher 的回調(diào)函數(shù)。

當(dāng)我們偵聽一個通過 reactive API 創(chuàng)建的響應(yīng)式對象時,內(nèi)部會執(zhí)行 traverse 函數(shù),如果這個對象非常復(fù)雜,比如嵌套層級很深,那么遞歸 traverse 就會有一定的性能耗時。因此如果我們需要偵聽這個復(fù)雜響應(yīng)式對象內(nèi)部的某個具體屬性,就可以想辦法減少 traverse 帶來的性能損耗。

比如剛才的例子,我們就可以直接偵聽 state.count.a.b 的變化:

watch(state.count.a, (newVal, oldVal) => { 
  console.log(newVal) 
}) 
state.count.a.b = 2 

這樣就可以減少內(nèi)部執(zhí)行 traverse 的次數(shù)。你可能會問,直接偵聽 state.count.a.b 可以嗎?答案是不行,因?yàn)?state.count.a.b 已經(jīng)是一個基礎(chǔ)數(shù)字類型了,不符合 source 要求的參數(shù)類型,所以會在非生產(chǎn)環(huán)境下報警告。

那么有沒有辦法優(yōu)化使得 traverse 不執(zhí)行呢?答案是可以的。我們可以偵聽一個 getter 函數(shù):

watch(() => state.count.a.b, (newVal, oldVal) => { 
  console.log(newVal) 
}) 
state.count.a.b = 2 

這樣函數(shù)內(nèi)部會訪問并返回 state.count.a.b,一次 traverse 都不會執(zhí)行并且依然可以偵聽到它的變化從而執(zhí)行 watcher 的回調(diào)函數(shù)。

構(gòu)造回調(diào)函數(shù)

處理完 watch API 第一個參數(shù) source 后,接下來處理第二個參數(shù) cb。

cb 是一個回調(diào)函數(shù),它有三個參數(shù):第一個 newValue 代表新值;第二個 oldValue 代表舊值。第三個參數(shù) onInvalidate,這個放在后面介紹。

其實(shí)這樣的 API 設(shè)計非常好理解,即偵聽一個值的變化,如果值變了就執(zhí)行回調(diào)函數(shù),回調(diào)函數(shù)里可以訪問到新值和舊值。

接下來我們來看一下構(gòu)造回調(diào)函數(shù)的處理邏輯:

let cleanup 
// 注冊無效回調(diào)函數(shù) 
const onInvalidate = (fn) => { 
  cleanup = runner.options.onStop = () => { 
    callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */) 
  } 
} 
// 舊值初始值 
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE /*{}*/ 
// 回調(diào)函數(shù) 
const applyCb = cb 
  ? () => { 
    // 組件銷毀,則直接返回 
    if (instance && instance.isUnmounted) { 
      return 
    } 
    // 求得新值 
    const newValue = runner() 
    if (deep || hasChanged(newValue, oldValue)) { 
      // 執(zhí)行清理函數(shù) 
      if (cleanup) { 
        cleanup() 
      } 
      callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [ 
        newValue, 
        // 第一次更改時傳遞舊值為 undefined 
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, 
        onInvalidate 
      ]) 
      // 更新舊值 
      oldValue = newValue 
    } 
  } 
  : void 0 

onInvalidate 函數(shù)用來注冊無效回調(diào)函數(shù) ,我們暫時不需要關(guān)注它,我們需要重點(diǎn)來看 applyCb。 這個函數(shù)實(shí)際上就是對 cb 做一層封裝,當(dāng)偵聽的值發(fā)生變化時就會執(zhí)行 applyCb 方法,我們來分析一下它的實(shí)現(xiàn)。

首先,watch API 和組件實(shí)例相關(guān),因?yàn)橥ǔN覀儠诮M件的 setup 函數(shù)中使用它,當(dāng)組件銷毀后,回調(diào)函數(shù) cb 不應(yīng)該被執(zhí)行而是直接返回。

接著,執(zhí)行 runner 求得新值,這里實(shí)際上就是執(zhí)行前面創(chuàng)建的 getter 函數(shù)求新值。

最后進(jìn)行判斷,如果是 deep 的情況或者新舊值發(fā)生了變化,則執(zhí)行回調(diào)函數(shù) cb,傳入?yún)?shù) newValue 和 oldValue。注意,第一次執(zhí)行的時候舊值的初始值是空數(shù)組或者 undefined。執(zhí)行完回調(diào)函數(shù) cb 后,把舊值 oldValue 再更新為 newValue,這是為了下一次的比對。

創(chuàng)建scheduler

接下來我們要分析創(chuàng)建 scheduler 過程。

scheduler 的作用是根據(jù)某種調(diào)度的方式去執(zhí)行某種函數(shù),在 watch API 中,主要影響到的是回調(diào)函數(shù)的執(zhí)行方式。我們來看一下它的實(shí)現(xiàn)邏輯:

const invoke = (fn) => fn() 
let scheduler 
if (flush === 'sync') { 
  // 同步 
  scheduler = invoke 
} 
else if (flush === 'pre') { 
  scheduler = job => { 
    if (!instance || instance.isMounted) { 
      // 進(jìn)入異步隊列,組件更新前執(zhí)行 
      queueJob(job) 
    } 
    else { 
      // 如果組件還沒掛載,則同步執(zhí)行確保在組件掛載前 
      job() 
    } 
  } 
} 
else { 
  // 進(jìn)入異步隊列,組件更新后執(zhí)行 
  scheduler = job => queuePostRenderEffect(job, instance && instance.suspense) 
} 

Watch API 的參數(shù)除了 source 和 cb,還支持第三個參數(shù) options,不同的配置決定了 watcher 的不同行為。前面我們也分析了 deep 為 true 的情況,除了 source 為 reactive 對象時會默認(rèn)把 deep 設(shè)置為 true,你也可以主動傳入第三個參數(shù),把 deep 設(shè)置為 true。

這里,scheduler 的創(chuàng)建邏輯受到了第三個參數(shù) Options 中的 flush 屬性值的影響,不同的 flush 決定了 watcher 的執(zhí)行時機(jī)。

  • 當(dāng) flush 為 sync 的時候,表示它是一個同步 watcher,即當(dāng)數(shù)據(jù)變化時同步執(zhí)行回調(diào)函數(shù)。
  • 當(dāng) flush 為 pre 的時候,回調(diào)函數(shù)通過 queueJob 的方式在組件更新之前執(zhí)行,如果組件還沒掛載,則同步執(zhí)行確?;卣{(diào)函數(shù)在組件掛載之前執(zhí)行。
  • 如果沒設(shè)置 flush,那么回調(diào)函數(shù)通過 queuePostRenderEffect 的方式在組件更新之后執(zhí)行。

queueJob 和 queuePostRenderEffect 在這里不是重點(diǎn),所以我們放到后面介紹??傊悻F(xiàn)在要記住,watcher 的回調(diào)函數(shù)是通過一定的調(diào)度方式執(zhí)行的。

創(chuàng)建effect

前面的分析我們提到了 runner,它其實(shí)就是 watcher 內(nèi)部創(chuàng)建的 effect 函數(shù),接下來,我們來分析它邏輯:

const runner = effect(getter, { 
  // 延時執(zhí)行 
  lazy: true, 
  // computed effect 可以優(yōu)先于普通的 effect 先運(yùn)行,比如組件渲染的 effect 
  computed: true, 
  onTrack, 
  onTrigger, 
  scheduler: applyCb ? () => scheduler(applyCb) : scheduler 
}) 
// 在組件實(shí)例中記錄這個 effect 
recordInstanceBoundEffect(runner) 
// 初次執(zhí)行 
if (applyCb) { 
  if (immediate) { 
    applyCb() 
  } 
  else { 
    // 求舊值 
    oldValue = runner() 
  } 
} 
else { 
  // 沒有 cb 的情況 
  runner() 
} 

這塊代碼邏輯是整個 watcher 實(shí)現(xiàn)的核心部分,即通過 effect API 創(chuàng)建一個副作用函數(shù) runner,我們需要關(guān)注以下幾點(diǎn)。

runner 是一個 computed effect。 因?yàn)?computed effect 可以優(yōu)先于普通的 effect(比如組件渲染的 effect)先運(yùn)行,這樣就可以實(shí)現(xiàn)當(dāng)配置 flush 為 pre 的時候,watcher 的執(zhí)行可以優(yōu)先于組件更新。

runner 執(zhí)行的方式。 runner 是 lazy 的,它不會在創(chuàng)建后立刻執(zhí)行。第一次手動執(zhí)行 runner 會執(zhí)行前面的 getter 函數(shù),訪問響應(yīng)式數(shù)據(jù)并做依賴收集。注意,此時activeEffect 就是 runner,這樣在后面更新響應(yīng)式數(shù)據(jù)時,就可以觸發(fā) runner 執(zhí)行 scheduler 函數(shù),以一種調(diào)度方式來執(zhí)行回調(diào)函數(shù)。

runner 的返回結(jié)果。 手動執(zhí)行 runner 就相當(dāng)于執(zhí)行了前面標(biāo)準(zhǔn)化的 getter 函數(shù),getter 函數(shù)的返回值就是 watcher 計算出的值,所以我們第一次執(zhí)行 runner 求得的值可以作為 oldValue。

配置了 immediate 的情況。 當(dāng)我們配置了 immediate ,創(chuàng)建完 watcher 會立刻執(zhí)行 applyCb 函數(shù),此時 oldValue 還是初始值,在 applyCb 執(zhí)行時也會執(zhí)行 runner 進(jìn)而執(zhí)行前面的 getter 函數(shù)做依賴收集,求得新值。

返回銷毀函數(shù)

最后,會返回偵聽器銷毀函數(shù),也就是 watch API 執(zhí)行后返回的函數(shù)。我們可以通過調(diào)用它來停止 watcher 對數(shù)據(jù)的偵聽。

return () => { 
  stop(runner) 
  if (instance) { 
    // 移除組件 effects 對這個 runner 的引用 
    remove(instance.effects, runner) 
  } 
} 
function stop(effect) { 
  if (effect.active) { 
    cleanup(effect) 
    if (effect.options.onStop) { 
      effect.options.onStop() 
    } 
    effect.active = false 
  } 
} 

銷毀函數(shù)內(nèi)部會執(zhí)行 stop 方法讓 runner 失活,并清理 runner 的相關(guān)依賴,這樣就可以停止對數(shù)據(jù)的偵聽。并且,如果是在組件中注冊的 watcher,也會移除組件 effects 對這個 runner 的引用。

異步任務(wù)隊列的設(shè)計

偵聽器的回調(diào)函數(shù)是以一種調(diào)度的方式執(zhí)行的,特別是當(dāng) flush 不是 sync 時,它會把回調(diào)函數(shù)執(zhí)行的任務(wù)推到一個異步隊列中執(zhí)行。接下來,我們就來分析異步執(zhí)行隊列的設(shè)計。分析之前,我們先來思考一下,為什么會需要異步隊列?

我們把之前的例子簡單修改一下:

import { reactive, watch } from 'vue' 
const state = reactive({ count: 0 }) 
watch(() => state.count, (count, prevCount) => { 
  console.log(count) 
}) 
state.count++ 
state.count++ 
state.count++ 

這里,我們修改了三次 state.count,那么 watcher 的回調(diào)函數(shù)會執(zhí)行三次嗎?

答案是不會,實(shí)際上只輸出了一次 count 的值,也就是最終計算的值 3。這在大多數(shù)場景下都是符合預(yù)期的,因?yàn)樵谝粋€ Tick(宏任務(wù)執(zhí)行的生命周期)內(nèi),即使多次修改偵聽的值,它的回調(diào)函數(shù)也只執(zhí)行一次。

組件的更新過程是異步的,我們知道修改模板中引用的響應(yīng)式對象的值時,會觸發(fā)組件的重新渲染,但是在一個 Tick 內(nèi),即使你多次修改多個響應(yīng)式對象的值,組件的重新渲染也只執(zhí)行一次。這是因?yàn)槿绻看胃聰?shù)據(jù)都觸發(fā)組件重新渲染,那么重新渲染的次數(shù)和代價都太高了。

那么,這是怎么做到的呢?我們先從異步任務(wù)隊列的創(chuàng)建說起。

異步任務(wù)隊列的創(chuàng)建

通過前面的分析我們知道,在創(chuàng)建一個 watcher 時,如果配置 flush 為 pre 或不配置 flush ,那么 watcher 的回調(diào)函數(shù)就會異步執(zhí)行。此時分別是通過 queueJob 和 queuePostRenderEffect 把回調(diào)函數(shù)推入異步隊列中的。

在不涉及 suspense 的情況下,queuePostRenderEffect 相當(dāng)于 queuePostFlushCb,我們來看它們的實(shí)現(xiàn):

// 異步任務(wù)隊列 
const queue = [] 
// 隊列任務(wù)執(zhí)行完后執(zhí)行的回調(diào)函數(shù)隊列 
const postFlushCbs = [] 
function queueJob(job) { 
  if (!queue.includes(job)) { 
    queue.push(job) 
    queueFlush() 
  } 
} 
function queuePostFlushCb(cb) { 
  if (!isArray(cb)) { 
    postFlushCbs.push(cb) 
  } 
  else { 
    // 如果是數(shù)組,把它拍平成一維 
    postFlushCbs.push(...cb) 
  } 
  queueFlush() 
} 

Vue.js 內(nèi)部維護(hù)了一個 queue 數(shù)組和一個 postFlushCbs 數(shù)組,其中 queue 數(shù)組用作異步任務(wù)隊列, postFlushCbs 數(shù)組用作異步任務(wù)隊列執(zhí)行完畢后的回調(diào)函數(shù)隊列。

執(zhí)行 queueJob 時會把這個任務(wù) job 添加到 queue 的隊尾,而執(zhí)行 queuePostFlushCb 時,會把這個 cb 回調(diào)函數(shù)添加到 postFlushCbs 的隊尾。它們在添加完畢后都執(zhí)行了 queueFlush 函數(shù),我們接著看它的實(shí)現(xiàn):

const p = Promise.resolve() 
// 異步任務(wù)隊列是否正在執(zhí)行 
let isFlushing = false 
// 異步任務(wù)隊列是否等待執(zhí)行 
let isFlushPending = false 
function nextTick(fn) { 
  return fn ? p.then(fn) : p 
} 
function queueFlush() { 
  if (!isFlushing && !isFlushPending) { 
    isFlushPending = true 
    nextTick(flushJobs) 
  } 
} 

可以看到,Vue.js 內(nèi)部還維護(hù)了 isFlushing 和 isFlushPending 變量,用來控制異步任務(wù)的刷新邏輯。

在 queueFlush 首次執(zhí)行時,isFlushing 和 isFlushPending 都是 false,此時會把 isFlushPending 設(shè)置為 true,并且調(diào)用 nextTick(flushJobs) 去執(zhí)行隊列里的任務(wù)。

因?yàn)?isFlushPending 的控制,這使得即使多次執(zhí)行 queueFlush,也不會多次去執(zhí)行 flushJobs。另外 nextTick 在 Vue.js 3.0 中的實(shí)現(xiàn)也是非常簡單,通過 Promise.resolve().then 去異步執(zhí)行 flushJobs。

因?yàn)?JavaScript 是單線程執(zhí)行的,這樣的異步設(shè)計使你在一個 Tick 內(nèi),可以多次執(zhí)行 queueJob 或者 queuePostFlushCb 去添加任務(wù),也可以保證在宏任務(wù)執(zhí)行完畢后的微任務(wù)階段執(zhí)行一次 flushJobs。

異步任務(wù)隊列的執(zhí)行

創(chuàng)建完任務(wù)隊列后,接下來要異步執(zhí)行這個隊列,我們來看一下 flushJobs 的實(shí)現(xiàn):

const getId = (job) => (job.id == null ? Infinity : job.id) 
function flushJobs(seen) { 
  isFlushPending = false 
  isFlushing = true 
  let job 
  if ((process.env.NODE_ENV !== 'production')) { 
    seen = seen || new Map() 
  } 
  // 組件的更新是先父后子 
  // 如果一個組件在父組件更新過程中卸載,它自身的更新應(yīng)該被跳過 
  queue.sort((a, b) => getId(a) - getId(b)) 
  while ((job = queue.shift()) !== undefined) { 
    if (job === null) { 
      continue 
    } 
    if ((process.env.NODE_ENV !== 'production')) { 
      checkRecursiveUpdates(seen, job) 
    } 
    callWithErrorHandling(job, null, 14 /* SCHEDULER */) 
  } 
  flushPostFlushCbs(seen) 
  isFlushing = false 
  // 一些 postFlushCb 執(zhí)行過程中會再次添加異步任務(wù),遞歸 flushJobs 會把它們都執(zhí)行完畢 
  if (queue.length || postFlushCbs.length) { 
    flushJobs(seen) 
  } 
} 

可以看到,flushJobs 函數(shù)開始執(zhí)行的時候,會把 isFlushPending 重置為 false,把 isFlushing 設(shè)置為 true 來表示正在執(zhí)行異步任務(wù)隊列。

對于異步任務(wù)隊列 queue,在遍歷執(zhí)行它們前會先對它們做一次從小到大的排序,這是因?yàn)閮蓚€主要原因:

  • 我們創(chuàng)建組件的過程是由父到子,所以創(chuàng)建組件副作用渲染函數(shù)也是先父后子,父組件的副作用渲染函數(shù)的 effect id 是小于子組件的,每次更新組件也是通過 queueJob 把 effect 推入異步任務(wù)隊列 queue 中的。所以為了保證先更新父組再更新子組件,要對 queue 做從小到大的排序。
  • 如果一個組件在父組件更新過程中被卸載,它自身的更新應(yīng)該被跳過。所以也應(yīng)該要保證先更新父組件再更新子組件,要對 queue 做從小到大的排序。

接下來,就是遍歷這個 queue,依次執(zhí)行隊列中的任務(wù)了,在遍歷過程中,注意有一個 checkRecursiveUpdates 的邏輯,它是用來在非生產(chǎn)環(huán)境下檢測是否有循環(huán)更新的,它的作用我們稍后會提。

遍歷完 queue 后,又會進(jìn)一步執(zhí)行 flushPostFlushCbs 方法去遍歷執(zhí)行所有推入到 postFlushCbs 的回調(diào)函數(shù):

function flushPostFlushCbs(seen) { 
  if (postFlushCbs.length) { 
    // 拷貝副本 
    const cbs = [...new Set(postFlushCbs)] 
    postFlushCbs.length = 0 
    if ((process.env.NODE_ENV !== 'production')) { 
      seen = seen || new Map() 
    } 
    for (let i = 0; i < cbs.length; i++) { 
      if ((process.env.NODE_ENV !== 'production')) {                                                       
        checkRecursiveUpdates(seen, cbs[i]) 
      } 
      cbs[i]() 
    } 
  } 
} 

注意這里遍歷前會通過 const cbs = [...new Set(postFlushCbs)] 拷貝一個 postFlushCbs 的副本,這是因?yàn)樵诒闅v的過程中,可能某些回調(diào)函數(shù)的執(zhí)行會再次修改 postFlushCbs,所以拷貝一個副本循環(huán)遍歷則不會受到 postFlushCbs 修改的影響。

遍歷完 postFlushCbs 后,會重置 isFlushing 為 false,因?yàn)橐恍?postFlushCb 執(zhí)行過程中可能會再次添加異步任務(wù),所以需要繼續(xù)判斷如果 queue 或者 postFlushCbs 隊列中還存在任務(wù),則遞歸執(zhí)行 flushJobs 把它們都執(zhí)行完畢。

檢測循環(huán)更新

前面我們提到了,在遍歷執(zhí)行異步任務(wù)和回調(diào)函數(shù)的過程中,都會在非生產(chǎn)環(huán)境下執(zhí)行 checkRecursiveUpdates 檢測是否有循環(huán)更新,它是用來解決什么問題的呢?

我們把之前的例子改寫一下:

import { reactive, watch } from 'vue' 
const state = reactive({ count: 0 }) 
watch(() => state.count, (count, prevCount) => { 
  state.count++ 
  console.log(count) 
}) 
state.count++ 

如果你去跑這個示例,你會在控制臺看到輸出了 101 次值,然后報了錯誤: Maximum recursive updates exceeded 。這是因?yàn)槲覀冊?watcher 的回調(diào)函數(shù)里更新了數(shù)據(jù),這樣會再一次進(jìn)入回調(diào)函數(shù),如果我們不加任何控制,那么回調(diào)函數(shù)會一直執(zhí)行,直到把內(nèi)存耗盡造成瀏覽器假死。

為了避免這種情況,Vue.js 實(shí)現(xiàn)了 checkRecursiveUpdates 方法:

const RECURSION_LIMIT = 100 
function checkRecursiveUpdates(seen, fn) { 
  if (!seen.has(fn)) { 
    seen.set(fn, 1) 
  } 
  else { 
    const count = seen.get(fn) 
    if (count > RECURSION_LIMIT) { 
      throw new Error('Maximum recursive updates exceeded. ' + 
        "You may have code that is mutating state in your component's " + 
        'render function or updated hook or watcher source function.') 
    } 
    else { 
      seen.set(fn, count + 1) 
    } 
  } 
} 

通過前面的代碼,我們知道 flushJobs 一開始便創(chuàng)建了 seen,它是一個 Map 對象,然后在 checkRecursiveUpdates 的時候會把任務(wù)添加到 seen 中,記錄引用計數(shù) count,初始值為 1,如果 postFlushCbs 再次添加了相同的任務(wù),則引用計數(shù) count 加 1,如果 count 大于我們定義的限制 100 ,則說明一直在添加這個相同的任務(wù)并超過了 100 次。那么,Vue.js 會拋出這個錯誤,因?yàn)樵谡5氖褂弥?,不?yīng)該出現(xiàn)這種情況,而我們上述的錯誤示例就會觸發(fā)這種報錯邏輯。

優(yōu)化:只用一個變量

到這里,異步隊列的設(shè)計就介紹完畢了,你可能會對 isFlushPending 和 isFlushing 有些疑問,為什么需要兩個變量來控制呢?

從語義上來看,isFlushPending 用于判斷是否在等待 nextTick 執(zhí)行 flushJobs,而 isFlushing 是判斷是否正在執(zhí)行任務(wù)隊列。

從功能上來看,它們的作用是為了確保以下兩點(diǎn):

  • 在一個 Tick 內(nèi)可以多次添加任務(wù)到隊列中,但是任務(wù)隊列會在 nextTick 后執(zhí)行;
  • 在執(zhí)行任務(wù)隊列的過程中,也可以添加新的任務(wù)到隊列中,并且在當(dāng)前 Tick 去執(zhí)行剩余的任務(wù)隊列。

但實(shí)際上,這里我們可以進(jìn)行優(yōu)化。在我看來,這里用一個變量就足夠了,我們來稍微修改一下源碼:

function queueFlush() { 
  if (!isFlushing) { 
    isFlushing = true 
    nextTick(flushJobs) 
  } 
} 
function flushJobs(seen) { 
  let job 
  if ((process.env.NODE_ENV !== 'production')) { 
    seen = seen || new Map() 
  } 
  queue.sort((a, b) => getId(a) - getId(b)) 
  while ((job = queue.shift()) !== undefined) { 
    if (job === null) { 
      continue 
    } 
    if ((process.env.NODE_ENV !== 'production')) { 
      checkRecursiveUpdates(seen, job) 
    } 
    callWithErrorHandling(job, null, 14 /* SCHEDULER */) 
  } 
  flushPostFlushCbs(seen) 
  if (queue.length || postFlushCbs.length) { 
    flushJobs(seen) 
  } 
  isFlushing = false 
} 

可以看到,我們只需要一個 isFlushing 來控制就可以實(shí)現(xiàn)相同的功能了。在執(zhí)行 queueFlush 的時候,判斷 isFlushing 為 false,則把它設(shè)置為 true,然后 nextTick 會執(zhí)行 flushJobs。在 flushJobs 函數(shù)執(zhí)行完成的最后,也就是所有的任務(wù)(包括后添加的)都執(zhí)行完畢,再設(shè)置 isFlushing 為 false。

了解完 watch API 和異步任務(wù)隊列的設(shè)計后,我們再來學(xué)習(xí)偵聽器提供的另一個 API—— watchEffect API。

watchEffect

watchEffect API 的作用是注冊一個副作用函數(shù),副作用函數(shù)內(nèi)部可以訪問到響應(yīng)式對象,當(dāng)內(nèi)部響應(yīng)式對象變化后再立即執(zhí)行這個函數(shù)。

可以先來看一個示例:

import { ref, watchEffect } from 'vue' 
const count = ref(0) 
watchEffect(() => console.log(count.value)) 
count.value++ 

它的結(jié)果是依次輸出 0 和 1。

watchEffect 和前面的 watch API 有哪些不同呢?主要有三點(diǎn):

  • 偵聽的源不同 。 watch API 可以偵聽一個或多個響應(yīng)式對象,也可以偵聽一個 getter 函數(shù),而 watchEffect API 偵聽的是一個普通函數(shù),只要內(nèi)部訪問了響應(yīng)式對象即可,這個函數(shù)并不需要返回響應(yīng)式對象。
  • 沒有回調(diào)函數(shù) 。 watchEffect API 沒有回調(diào)函數(shù),副作用函數(shù)的內(nèi)部響應(yīng)式對象發(fā)生變化后,會再次執(zhí)行這個副作用函數(shù)。
  • 立即執(zhí)行 。 watchEffect API 在創(chuàng)建好 watcher 后,會立刻執(zhí)行它的副作用函數(shù),而 watch API 需要配置 immediate 為 true,才會立即執(zhí)行回調(diào)函數(shù)。

對 watchEffect API 有大體了解后,我們來看一下在我整理的 watchEffect 場景下, doWatch 函數(shù)的簡化版實(shí)現(xiàn):

function watchEffect(effect, options) { 
  return doWatch(effect, null, options); 
} 
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { 
  instance = currentInstance; 
  let getter; 
  if (isFunction(source)) { 
    getter = () => { 
      if (instance && instance.isUnmounted) { 
        return; 
      } 
       // 執(zhí)行清理函數(shù) 
      if (cleanup) { 
        cleanup(); 
      } 
      // 執(zhí)行 source 函數(shù),傳入 onInvalidate 作為參數(shù) 
      return callWithErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onInvalidate]); 
    }; 
  } 
  let cleanup; 
  const onInvalidate = (fn) => { 
    cleanup = runner.options.onStop = () => { 
      callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); 
    }; 
  }; 
  let scheduler; 
  // 創(chuàng)建 scheduler 
  if (flush === 'sync') { 
    scheduler = invoke; 
  } 
  else if (flush === 'pre') { 
    scheduler = job => { 
      if (!instance || instance.isMounted) { 
        queueJob(job); 
      } 
      else { 
        job(); 
      } 
    }; 
  } 
  else { 
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense); 
  } 
  // 創(chuàng)建 runner 
  const runner = effect(getter, { 
    lazy: true, 
    computed: true, 
    onTrack, 
    onTrigger, 
    scheduler 
  }); 
  recordInstanceBoundEffect(runner); 
   
  // 立即執(zhí)行 runner 
  runner(); 
   
  // 返回銷毀函數(shù) 
  return () => { 
    stop(runner); 
    if (instance) { 
      remove(instance.effects, runner); 
    } 
  }; 
} 

可以看到,getter 函數(shù)就是對 source 函數(shù)的簡單封裝,它會先判斷組件實(shí)例是否已經(jīng)銷毀,然后每次執(zhí)行 source 函數(shù)前執(zhí)行 cleanup 清理函數(shù)。

watchEffect 內(nèi)部創(chuàng)建的 runner 對應(yīng)的 scheduler 對象就是 scheduler 函數(shù)本身,這樣它再次執(zhí)行時,就會執(zhí)行這個 scheduler 函數(shù),并且傳入 runner 函數(shù)作為參數(shù),其實(shí)就是按照一定的調(diào)度方式去執(zhí)行基于 source 封裝的 getter 函數(shù)。

創(chuàng)建完 runner 后就立刻執(zhí)行了 runner,其實(shí)就是內(nèi)部同步執(zhí)行了基于 source 封裝的 getter 函數(shù)。

在執(zhí)行 source 函數(shù)的時候,會傳入一個 onInvalidate 函數(shù)作為參數(shù),接下來我們就來分析它的作用。

注冊無效回調(diào)函數(shù)

有些時候,watchEffect 會注冊一個副作用函數(shù),在函數(shù)內(nèi)部可以做一些異步操作,但是當(dāng)這個 watcher 停止后,如果我們想去對這個異步操作做一些額外事情(比如取消這個異步操作),我們可以通過 onInvalidate 參數(shù)注冊一個無效函數(shù)。

import {ref, watchEffect } from 'vue' 
const id = ref(0) 
watchEffect(onInvalidate => { 
  // 執(zhí)行異步操作 
  const token = performAsyncOperation(id.value) 
  onInvalidate(() => { 
    // 如果 id 發(fā)生變化或者 watcher 停止了,則執(zhí)行邏輯取消前面的異步操作 
    token.cancel() 
  }) 
}) 

我們利用 watchEffect 注冊了一個副作用函數(shù),它有一個 onInvalidate 參數(shù)。在這個函數(shù)內(nèi)部通過 performAsyncOperation 執(zhí)行某些異步操作,并且訪問了 id 這個響應(yīng)式對象,然后通過 onInvalidate 注冊了一個回調(diào)函數(shù)。

如果 id 發(fā)生變化或者 watcher 停止了,這個回調(diào)函數(shù)將會執(zhí)行,然后執(zhí)行 token.cancel 取消之前的異步操作。

我們來回顧 onInvalidate 在 doWatch 中的實(shí)現(xiàn):

const onInvalidate = (fn) => { 
  cleanup = runner.options.onStop = () => { 
    callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); 
  }; 
}; 

實(shí)際上,當(dāng)你執(zhí)行 onInvalidate 的時候,就是注冊了一個 cleanup 和 runner 的 onStop 方法,這個方法內(nèi)部會執(zhí)行 fn,也就是你注冊的無效回調(diào)函數(shù)。

也就是說當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化,會執(zhí)行 cleanup 方法,當(dāng) watcher 被停止,會執(zhí)行 onStop 方法,這兩者都會執(zhí)行注冊的無效回調(diào)函數(shù) fn。

通過這種方式,Vue.js 就很好地實(shí)現(xiàn)了 watcher 注冊無效回調(diào)函數(shù)的需求。

總結(jié)

偵聽器的內(nèi)部設(shè)計很巧妙,我們可以偵聽響應(yīng)式數(shù)據(jù)的變化,內(nèi)部創(chuàng)建 effect runner,首次執(zhí)行 runner 做依賴收集,然后在數(shù)據(jù)發(fā)生變化后,以某種調(diào)度方式去執(zhí)行回調(diào)函數(shù)。

相比于計算屬性,偵聽器更適合用于在數(shù)據(jù)變化后執(zhí)行某段邏輯的場景,而計算屬性則用于一個數(shù)據(jù)依賴另外一些數(shù)據(jù)計算而來的場景。

以上就是深入了解Vue3中偵聽器watcher的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Vue3 偵聽器watcher的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解vue頁面首次加載緩慢原因及解決方案

    詳解vue頁面首次加載緩慢原因及解決方案

    這篇文章主要介紹了詳解vue頁面首次加載緩慢原因及解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • vue自定v-model實(shí)現(xiàn)表單數(shù)據(jù)雙向綁定問題

    vue自定v-model實(shí)現(xiàn)表單數(shù)據(jù)雙向綁定問題

    vue.js的一大功能便是實(shí)現(xiàn)數(shù)據(jù)的雙向綁定。這篇文章主要介紹了vue自定v-model實(shí)現(xiàn) 表單數(shù)據(jù)雙向綁定的相關(guān)知識,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-09-09
  • 解決vue-cli腳手架打包后vendor文件過大的問題

    解決vue-cli腳手架打包后vendor文件過大的問題

    今天小編就為大家分享一篇解決vue-cli腳手架打包后vendor文件過大的問題。具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-09-09
  • vue2.X組件學(xué)習(xí)心得(新手必看篇)

    vue2.X組件學(xué)習(xí)心得(新手必看篇)

    下面小編就為大家?guī)硪黄獀ue2.X組件學(xué)習(xí)心得(新手必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-07-07
  • 解決vue中el-tab-pane切換的問題

    解決vue中el-tab-pane切換的問題

    這篇文章主要介紹了解決vue中el-tab-pane切換的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-07-07
  • vue封裝tree組件實(shí)現(xiàn)搜索功能

    vue封裝tree組件實(shí)現(xiàn)搜索功能

    本文主要介紹了vue封裝tree組件實(shí)現(xiàn)搜索功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05
  • Vue實(shí)現(xiàn)首頁banner自動輪播效果

    Vue實(shí)現(xiàn)首頁banner自動輪播效果

    這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)首頁banner自動輪播效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • vue實(shí)現(xiàn)翻牌動畫

    vue實(shí)現(xiàn)翻牌動畫

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)翻牌動畫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • 利用Vue模擬實(shí)現(xiàn)element-ui的分頁器效果

    利用Vue模擬實(shí)現(xiàn)element-ui的分頁器效果

    這篇文章主要為大家詳細(xì)介紹了如何利用Vue模擬實(shí)現(xiàn)element-ui的分頁器效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動手嘗試一下
    2022-11-11
  • vue自定義table表如何實(shí)現(xiàn)內(nèi)容上下循環(huán)滾動

    vue自定義table表如何實(shí)現(xiàn)內(nèi)容上下循環(huán)滾動

    這篇文章主要介紹了vue自定義table表如何實(shí)現(xiàn)內(nèi)容上下循環(huán)滾動問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10

最新評論