最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文詳解Vue3的watch是如何實(shí)現(xiàn)監(jiān)聽的

 更新時(shí)間:2024年11月26日 15:07:56   作者:前端歐陽(yáng)  
watch這個(gè)API大家都很熟悉,今天這篇文章小編來帶你搞清楚Vue3的watch是如何實(shí)現(xiàn)對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行監(jiān)聽的,希望對(duì)大家有一定的幫助

前言

watch這個(gè)API大家都很熟悉,今天這篇文章歐陽(yáng)來帶你搞清楚Vue3的watch是如何實(shí)現(xiàn)對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行監(jiān)聽的。注:本文使用的Vue版本為3.5.13。

看個(gè)demo

我們來看個(gè)簡(jiǎn)單的demo,代碼如下:

<template>
  <button @click="count++">count++</button>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (preVal, curVal) => {
  console.log("count is changed", preVal, curVal);
});
</script>

這個(gè)demo很簡(jiǎn)單,使用watch監(jiān)聽了響應(yīng)式變量count,在watch回調(diào)中進(jìn)行了console打印。如何有個(gè)button按鈕,點(diǎn)擊后會(huì)count++。

開始打斷點(diǎn)

現(xiàn)在我們第一個(gè)斷點(diǎn)應(yīng)該打在哪里呢?

我們要看watch的實(shí)現(xiàn),那么當(dāng)然是給我們demo中的watch函數(shù)打個(gè)斷點(diǎn)。

首先執(zhí)行yarn dev將我們的demo跑起來,然后在瀏覽器的network面板中找到對(duì)應(yīng)的vue文件,右鍵點(diǎn)擊Open in Sources panel就可以在source面板中打開我們的代碼啦。如下圖

然后給watch函數(shù)打個(gè)斷點(diǎn),如下圖:

接著刷新頁(yè)面,此時(shí)代碼將會(huì)停留在斷點(diǎn)出。將斷點(diǎn)走進(jìn)watch函數(shù),代碼如下:

function watch(source, cb, options) {
  return doWatch(source, cb, options);
}

從上面的代碼可以看到在watch函數(shù)中直接返回了doWatch函數(shù)。

將斷點(diǎn)走進(jìn)doWatch函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下(為了方便大家理解,本文中會(huì)將scheduler任務(wù)調(diào)度相關(guān)的代碼移除掉,因?yàn)檫@個(gè)不影響watch的主流程):

function doWatch(source, cb, options = EMPTY_OBJ) {
  const baseWatchOptions = extend({}, options);
  const watchHandle = baseWatch(source, cb, baseWatchOptions);
  return watchHandle;
}

從上面的代碼可以看到底層實(shí)際是在執(zhí)行baseWatch函數(shù),而這個(gè)baseWatch就是由@vue/reactivity包中導(dǎo)出的watch函數(shù)。關(guān)于這個(gè)baseWatch函數(shù)的由來可以看看歐陽(yáng)之前的文章: Vue3.5新增的baseWatch讓watch函數(shù)和Vue組件徹底分手

baseWatch函數(shù)

將斷點(diǎn)走進(jìn)baseWatch函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:

const INITIAL_WATCHER_VALUE = {}

function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ
): WatchHandle {
  let effect: ReactiveEffect;
  let getter: () => any;

  if (isRef(source)) {
    getter = () => source.value;
  }

  let oldValue: any = INITIAL_WATCHER_VALUE;

  const job = () => {
    if (cb) {
      const newValue = effect.run();
      if (hasChanged(newValue, oldValue)) {
        const args = [
          newValue,
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          boundCleanup,
        ];

        cb(...args);
        oldValue = newValue;
      }
    }
  };
  effect = new ReactiveEffect(getter);
  effect.scheduler = job;

  oldValue = effect.run();
}

首先定義了兩個(gè)變量effectgetter,effectReactiveEffect類的實(shí)例。

接著就是使用isRef(source)判斷watch監(jiān)聽的是不是一個(gè)ref變量,如果是就將getter函數(shù)賦值為getter = () => source.value。這么做的原因是為了保持一致(watch也可以直接監(jiān)聽一個(gè)getter函數(shù)),并且后面會(huì)對(duì)這個(gè)getter函數(shù)進(jìn)行讀操作觸發(fā)依賴收集。

我們知道watch的回調(diào)中有oldValuenewValue這兩個(gè)字段,在watch函數(shù)內(nèi)部有個(gè)字段也名為oldValue用于存舊的值。

接著就是定義了一個(gè)job函數(shù),我們先不看里面的代碼,執(zhí)行這個(gè)job函數(shù)就會(huì)執(zhí)行watch的回調(diào)。

然后執(zhí)行effect = new ReactiveEffect(getter),這個(gè)ReactiveEffect類是一個(gè)底層的類。在Vue的設(shè)計(jì)中,所有的訂閱者都是繼承的這個(gè)ReactiveEffect類。比如watchEffectcomputed()、render函數(shù)等。

在我們這個(gè)場(chǎng)景中new ReactiveEffect時(shí)傳入的getter函數(shù)就是getter = () => source.value,這里的source就是watch監(jiān)聽的響應(yīng)式變量count。

接著將job函數(shù)賦值給effect.scheduler屬性,在ReactiveEffect類中依賴觸發(fā)時(shí)就會(huì)執(zhí)行effect.scheduler方法(接下來會(huì)講)。

最后就是執(zhí)行effect.run()拿到初始化時(shí)watch監(jiān)聽變量的值,這個(gè)run方法也是在ReactiveEffect類中。接下來也會(huì)講。

ReactiveEffect類

前面我們講過了ReactiveEffect是Vue的一個(gè)底層類,所有的訂閱者都是繼承的這個(gè)類。將斷點(diǎn)走進(jìn)ReactiveEffect類,在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:

class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
  constructor(fn) {
    this.fn = fn;
  }

  run(): T {
    const prevEffect = activeSub;
    activeSub = this;
    try {
      return this.fn();
    } finally {
      activeSub = prevEffect;
    }
  }

  trigger(): void {
    this.scheduler();
  }
}

在new一個(gè)ReactiveEffect實(shí)例時(shí)傳入的getter函數(shù)會(huì)賦值給實(shí)例的fn方法。(實(shí)際的ReactiveEffect代碼比這個(gè)要復(fù)雜很多,感興趣的同學(xué)可以去看源代碼)

我們回到前面講過的baseWatch函數(shù)中的最后一塊:oldValue = effect.run()。這里執(zhí)行了effect實(shí)例的run方法拿到watch監(jiān)聽變量的值,并且賦值給oldValue變量。

因?yàn)槲覀內(nèi)绻皇褂?code>immediate: true,那么Vue會(huì)等watch監(jiān)聽的變量改變后才會(huì)觸發(fā)watch回調(diào),回調(diào)中有個(gè)字段叫oldValue,這個(gè)oldValue就是初始化時(shí)執(zhí)行run方法拿到的。

比如我們這里count初始化的值是0,初始化執(zhí)行oldValue = effect.run()后就會(huì)給oldValue賦值為0。當(dāng)點(diǎn)擊count++按鈕后,count的值就變成了1,所以在watch回調(diào)第一次觸發(fā)的時(shí)候他就知道oldValue的值是0啦。

除此之外,在run方法中還有收集依賴的作用。Vue維護(hù)了一個(gè)全局變量activeSub表示當(dāng)前active的訂閱者是誰,在同一時(shí)間只可能有一個(gè)active的訂閱者,不然觸發(fā)get攔截進(jìn)行依賴收集時(shí)就不知道該把哪個(gè)訂閱者給收集了。

run方法中將當(dāng)前的activeSub給存起來,等下面的代碼執(zhí)行完了后將全局變量activeSub改回去。

接著就是執(zhí)行activeSub = this;將當(dāng)前的watch設(shè)置為全局變量activeSub。

接下來就是執(zhí)行return this.fn(),前面我們講過了這個(gè)this.fn()方法就是watch監(jiān)聽的getter函數(shù)。由于我們watch監(jiān)聽的是一個(gè)響應(yīng)式變量count,在前面處理后他的getter函數(shù)就是getter = () => source.value;。這里的source就是watch監(jiān)聽的變量,這個(gè)getter函數(shù)實(shí)際就是getter = () => count.value;

那么這里執(zhí)行return this.fn()就是執(zhí)行() => count.value,將會(huì)觸發(fā)響應(yīng)式變量count的get攔截。在get攔截中會(huì)進(jìn)行依賴收集,由于此時(shí)的全局變量activeSub已經(jīng)變成了訂閱者watch,所以響應(yīng)式變量count在依賴收集的過程中收集的訂閱者就是watch。這樣響應(yīng)式變量count就和訂閱者watch建立了依賴收集的關(guān)系。關(guān)于Vue3.5依賴收集和依賴觸發(fā)可以看看歐陽(yáng)之前的文章: 看不懂來打我!讓性能提升56%的Vue3.5響應(yīng)式重構(gòu)

當(dāng)我們點(diǎn)擊count++后會(huì)修改響應(yīng)式變量count的值,就會(huì)進(jìn)行依賴觸發(fā),經(jīng)過一堆操作后最后就會(huì)執(zhí)行到這里的trigger方法中。在trigger方法中直接執(zhí)行this.scheduler(),在前面已經(jīng)對(duì)scheduler方法進(jìn)行了賦值,回憶一下baseWatch函數(shù)的代碼。如下:

const INITIAL_WATCHER_VALUE = {}

function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ
): WatchHandle {
  let effect: ReactiveEffect;
  let getter: () => any;

  if (isRef(source)) {
    getter = () => source.value;
  }

  let oldValue: any = INITIAL_WATCHER_VALUE;

  const job = () => {
    if (cb) {
      const newValue = effect.run();
      if (hasChanged(newValue, oldValue)) {
        const args = [
          newValue,
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          boundCleanup,
        ];

        cb(...args);
        oldValue = newValue;
      }
    }
  };
  effect = new ReactiveEffect(getter);
  effect.scheduler = job;

  oldValue = effect.run();
}

這里將job函數(shù)賦值給effect.scheduler方法,所以當(dāng)響應(yīng)式變量count的值改變后實(shí)際就是在執(zhí)行這里的job函數(shù)。

job函數(shù)中首先判斷是否有傳入watch的callback函數(shù),然后執(zhí)行const newValue = effect.run()

執(zhí)行這行代碼有兩個(gè)作用:

第一個(gè)作用是重新執(zhí)行g(shù)etter函數(shù),也就是getter = () => count.value;,拿到最新count的值,將其賦值給newValue。

第二個(gè)作用是watch除了監(jiān)聽響應(yīng)式變量之外還可以監(jiān)聽一個(gè)getter函數(shù),那么在getter函數(shù)中就可以類似computed一樣在某些條件下監(jiān)聽變量A,某些條件下監(jiān)聽變量B。這里的第二個(gè)作用是重新收集依賴,因?yàn)榇藭r(shí)watch可能從監(jiān)聽變量A變成了監(jiān)聽變量B。

接著就是執(zhí)行if (hasChanged(newValue, oldValue))判斷watch監(jiān)聽的變量新的值和舊的值是否相等,如果不相等才去執(zhí)行cb(...args)觸發(fā)watch的回調(diào)。最后就是將當(dāng)前的newValue賦值給oldValue,下次觸發(fā)watch回調(diào)時(shí)作為oldValue字段。

總結(jié)

這篇文章講了watch如何對(duì)響應(yīng)式變量進(jìn)行監(jiān)聽,其實(shí)底層依賴的是@vue/reactivity包的baseWatch函數(shù)。在baseWatch函數(shù)中會(huì)使用ReactiveEffect類new一個(gè)effect實(shí)例,這個(gè)ReactiveEffect類是一個(gè)底層的類,Vue的訂閱者都是基于這個(gè)類去實(shí)現(xiàn)的。

如果沒有使用immediate: true,初始化時(shí)會(huì)去執(zhí)行一次effect.run()對(duì)watch監(jiān)聽的響應(yīng)式變量進(jìn)行讀操作并且將其賦值給oldValue。讀操作會(huì)觸發(fā)get攔截進(jìn)行響應(yīng)式變量的依賴收集,會(huì)將當(dāng)前watch作為訂閱者進(jìn)行收集。

當(dāng)響應(yīng)式變量的值改變后會(huì)觸發(fā)set攔截,進(jìn)而依賴觸發(fā)。前一步將watch也作為訂閱者進(jìn)行了收集,依賴觸發(fā)時(shí)也會(huì)通知到watch,所以此時(shí)會(huì)執(zhí)行watch中的job函數(shù)。在job函數(shù)中會(huì)再次執(zhí)行effect.run()拿到響應(yīng)式變量最新的值賦值給newValue,同時(shí)再次進(jìn)行依賴收集。如果oldValuenewValue不相等,那么就觸發(fā)watch的回調(diào),并且將oldValuenewValue作為參數(shù)傳過去。

到此這篇關(guān)于一文詳解Vue3的watch是如何實(shí)現(xiàn)監(jiān)聽的的文章就介紹到這了,更多相關(guān)Vue3 watch實(shí)現(xiàn)監(jiān)聽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

合水县| 玉田县| 濉溪县| 西贡区| 搜索| 惠东县| 陆川县| 德格县| 南康市| 乌什县| 壤塘县| 玉溪市| 西华县| 钟祥市| 长白| 天门市| 桐梓县| 融水| 买车| 芮城县| 吴忠市| 富蕴县| 乌鲁木齐市| 巢湖市| 沅江市| 崇左市| 象山县| 东宁县| 慈溪市| 民乐县| 巩留县| 林州市| 孙吴县| 务川| 清流县| 宜丰县| 搜索| 揭东县| 南阳市| 梅州市| 崇州市|