欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

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

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

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

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

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

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

在 Vue.js 3.0 中,雖然你仍可以使用 watch 選項(xiàng),但針對(duì) 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 可以偵聽一個(gè) getter 函數(shù),但是它必須返回一個(gè)響應(yīng)式對(duì)象,當(dāng)該響應(yīng)式對(duì)象更新后,會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。

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

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

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

3.watch API 還可以直接偵聽多個(gè)響應(yīng)式對(duì)象,任意一個(gè)響應(yīng)式對(duì)象更新后,就會(huì)執(zhí)行對(duì)應(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 更新,會(huì)觸發(fā)此回調(diào)函數(shù) 
}) 

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

偵聽器的言下之意就是,當(dāng)偵聽的對(duì)象或者函數(shù)發(fā)生了變化則自動(dòng)執(zhí)行某個(gè)回調(diào)函數(shù),這和副作用函數(shù) effect 很像, 那它的內(nèi)部實(shí)現(xiàn)是不是依賴了 effect 呢?帶著這個(gè)疑問,我們來探究 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 時(shí)序執(zhí)行函數(shù) 
  // 創(chuàng)建 effect 副作用函數(shù) 
  // 返回偵聽器銷毀函數(shù) 
}    

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

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

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

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

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

// source 不合法的時(shí)候會(huì)報(bào)警告 
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 對(duì)象,則創(chuàng)建一個(gè)訪問 source.value 的 getter 函數(shù);
  • 如果 source 是 reactive 對(duì)象,則創(chuàng)建一個(gè)訪問 source 的 getter 函數(shù),并設(shè)置 deep 為 true(deep 的作用我稍后會(huì)說)
  • 如果 source 是一個(gè)函數(shù),則會(huì)進(jìn)一步判斷第二個(gè)參數(shù) cb 是否存在,對(duì)于 watch API 來說,cb 是一定存在且是一個(gè)回調(diào)函數(shù),這種情況下,getter 就是一個(gè)簡單的對(duì) source 函數(shù)封裝的函數(shù)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

let cleanup 
// 注冊(cè)無效回調(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, 
        // 第一次更改時(shí)傳遞舊值為 undefined 
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, 
        onInvalidate 
      ]) 
      // 更新舊值 
      oldValue = newValue 
    } 
  } 
  : void 0 

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

首先,watch API 和組件實(shí)例相關(guān),因?yàn)橥ǔN覀儠?huì)在組件的 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í)候舊值的初始值是空數(shù)組或者 undefined。執(zhí)行完回調(diào)函數(shù) cb 后,把舊值 oldValue 再更新為 newValue,這是為了下一次的比對(duì)。

創(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)入異步隊(duì)列,組件更新前執(zhí)行 
      queueJob(job) 
    } 
    else { 
      // 如果組件還沒掛載,則同步執(zhí)行確保在組件掛載前 
      job() 
    } 
  } 
} 
else { 
  // 進(jìn)入異步隊(duì)列,組件更新后執(zhí)行 
  scheduler = job => queuePostRenderEffect(job, instance && instance.suspense) 
} 

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

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

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

queueJob 和 queuePostRenderEffect 在這里不是重點(diǎn),所以我們放到后面介紹??傊?,你現(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, { 
  // 延時(shí)執(zhí)行 
  lazy: true, 
  // computed effect 可以優(yōu)先于普通的 effect 先運(yùn)行,比如組件渲染的 effect 
  computed: true, 
  onTrack, 
  onTrigger, 
  scheduler: applyCb ? () => scheduler(applyCb) : scheduler 
}) 
// 在組件實(shí)例中記錄這個(gè) effect 
recordInstanceBoundEffect(runner) 
// 初次執(zhí)行 
if (applyCb) { 
  if (immediate) { 
    applyCb() 
  } 
  else { 
    // 求舊值 
    oldValue = runner() 
  } 
} 
else { 
  // 沒有 cb 的情況 
  runner() 
} 

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

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

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

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

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

返回銷毀函數(shù)

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

return () => { 
  stop(runner) 
  if (instance) { 
    // 移除組件 effects 對(duì)這個(gè) 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)部會(huì)執(zhí)行 stop 方法讓 runner 失活,并清理 runner 的相關(guān)依賴,這樣就可以停止對(duì)數(shù)據(jù)的偵聽。并且,如果是在組件中注冊(cè)的 watcher,也會(huì)移除組件 effects 對(duì)這個(gè) runner 的引用。

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

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

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

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ù)會(huì)執(zhí)行三次嗎?

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

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

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

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

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

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

// 異步任務(wù)隊(duì)列 
const queue = [] 
// 隊(duì)列任務(wù)執(zhí)行完后執(zhí)行的回調(diào)函數(shù)隊(duì)列 
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ù)了一個(gè) queue 數(shù)組和一個(gè) postFlushCbs 數(shù)組,其中 queue 數(shù)組用作異步任務(wù)隊(duì)列, postFlushCbs 數(shù)組用作異步任務(wù)隊(duì)列執(zhí)行完畢后的回調(diào)函數(shù)隊(duì)列。

執(zhí)行 queueJob 時(shí)會(huì)把這個(gè)任務(wù) job 添加到 queue 的隊(duì)尾,而執(zhí)行 queuePostFlushCb 時(shí),會(huì)把這個(gè) cb 回調(diào)函數(shù)添加到 postFlushCbs 的隊(duì)尾。它們?cè)谔砑油戤吅蠖紙?zhí)行了 queueFlush 函數(shù),我們接著看它的實(shí)現(xiàn):

const p = Promise.resolve() 
// 異步任務(wù)隊(duì)列是否正在執(zhí)行 
let isFlushing = false 
// 異步任務(wù)隊(duì)列是否等待執(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í)行時(shí),isFlushing 和 isFlushPending 都是 false,此時(shí)會(huì)把 isFlushPending 設(shè)置為 true,并且調(diào)用 nextTick(flushJobs) 去執(zhí)行隊(duì)列里的任務(wù)。

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

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

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

創(chuàng)建完任務(wù)隊(duì)列后,接下來要異步執(zhí)行這個(gè)隊(duì)列,我們來看一下 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() 
  } 
  // 組件的更新是先父后子 
  // 如果一個(gè)組件在父組件更新過程中卸載,它自身的更新應(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í)行過程中會(huì)再次添加異步任務(wù),遞歸 flushJobs 會(huì)把它們都執(zhí)行完畢 
  if (queue.length || postFlushCbs.length) { 
    flushJobs(seen) 
  } 
} 

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

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

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

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

遍歷完 queue 后,又會(huì)進(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]() 
    } 
  } 
} 

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

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

檢測(cè)循環(huán)更新

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

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

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

如果你去跑這個(gè)示例,你會(huì)在控制臺(tái)看到輸出了 101 次值,然后報(bào)了錯(cuò)誤: Maximum recursive updates exceeded 。這是因?yàn)槲覀冊(cè)?watcher 的回調(diào)函數(shù)里更新了數(shù)據(jù),這樣會(huì)再一次進(jìn)入回調(diào)函數(shù),如果我們不加任何控制,那么回調(diào)函數(shù)會(huì)一直執(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,它是一個(gè) Map 對(duì)象,然后在 checkRecursiveUpdates 的時(shí)候會(huì)把任務(wù)添加到 seen 中,記錄引用計(jì)數(shù) count,初始值為 1,如果 postFlushCbs 再次添加了相同的任務(wù),則引用計(jì)數(shù) count 加 1,如果 count 大于我們定義的限制 100 ,則說明一直在添加這個(gè)相同的任務(wù)并超過了 100 次。那么,Vue.js 會(huì)拋出這個(gè)錯(cuò)誤,因?yàn)樵谡5氖褂弥?,不?yīng)該出現(xiàn)這種情況,而我們上述的錯(cuò)誤示例就會(huì)觸發(fā)這種報(bào)錯(cuò)邏輯。

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

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

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

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

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

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

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 
} 

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

了解完 watch API 和異步任務(wù)隊(duì)列的設(shè)計(jì)后,我們?cè)賮韺W(xué)習(xí)偵聽器提供的另一個(gè) API—— watchEffect API。

watchEffect

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

可以先來看一個(gè)示例:

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

對(duì) watchEffect API 有大體了解后,我們來看一下在我整理的 watchEffect 場(chǎng)景下, 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ù)就是對(duì) source 函數(shù)的簡單封裝,它會(huì)先判斷組件實(shí)例是否已經(jīng)銷毀,然后每次執(zhí)行 source 函數(shù)前執(zhí)行 cleanup 清理函數(shù)。

watchEffect 內(nèi)部創(chuàng)建的 runner 對(duì)應(yīng)的 scheduler 對(duì)象就是 scheduler 函數(shù)本身,這樣它再次執(zhí)行時(shí),就會(huì)執(zhí)行這個(gè) 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ù)的時(shí)候,會(huì)傳入一個(gè) onInvalidate 函數(shù)作為參數(shù),接下來我們就來分析它的作用。

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

有些時(shí)候,watchEffect 會(huì)注冊(cè)一個(gè)副作用函數(shù),在函數(shù)內(nèi)部可以做一些異步操作,但是當(dāng)這個(gè) watcher 停止后,如果我們想去對(duì)這個(gè)異步操作做一些額外事情(比如取消這個(gè)異步操作),我們可以通過 onInvalidate 參數(shù)注冊(cè)一個(gè)無效函數(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 注冊(cè)了一個(gè)副作用函數(shù),它有一個(gè) onInvalidate 參數(shù)。在這個(gè)函數(shù)內(nèi)部通過 performAsyncOperation 執(zhí)行某些異步操作,并且訪問了 id 這個(gè)響應(yīng)式對(duì)象,然后通過 onInvalidate 注冊(cè)了一個(gè)回調(diào)函數(shù)。

如果 id 發(fā)生變化或者 watcher 停止了,這個(gè)回調(diào)函數(shù)將會(huì)執(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 的時(shí)候,就是注冊(cè)了一個(gè) cleanup 和 runner 的 onStop 方法,這個(gè)方法內(nèi)部會(huì)執(zhí)行 fn,也就是你注冊(cè)的無效回調(diào)函數(shù)。

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

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

總結(jié)

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

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

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

相關(guān)文章

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

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

    這篇文章主要介紹了詳解vue頁面首次加載緩慢原因及解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2018-09-09
  • 解決vue-cli腳手架打包后vendor文件過大的問題

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最新評(píng)論