Vue3 學(xué)習(xí)筆記之 watchEffect

最近在看 Vue3 的一些新 feature酌畜,順道學(xué)習(xí)了一些 hooks 編程的思想羊异,感覺(jué)挺有啟發(fā)的怎顾。今天就以 watchEffect 這個(gè)很小的 case 為例螟够,開(kāi)啟我的 Vue3 學(xué)習(xí)筆記灾梦。

Vue2 v.s. Vue3

對(duì)所有初學(xué)者來(lái)說(shuō),Vue2 到 Vue3 最直觀的改變就是 Composition API——幾乎所有的 Vue2 options 方法都被放到了 setup 函數(shù)里:

+ import { onMounted, reactive, watchEffect } from 'vue'

export default {
  name: "App",

+  setup( props ) {
+    const state = reactive({ /*...*/ });
+    onMounted(() => { /*...*/ });
+    watchEffect(() => { /*...*/ });
+    return { state };
+  },

-  data: () => ({ state: /*...*/ }),
-  mounted(){ /*...*/ },
-  watch: { /*...*/ },
};

這是一個(gè)比較大的風(fēng)格轉(zhuǎn)變妓笙,通俗來(lái)說(shuō)若河,就是從基于對(duì)象的編程(OOP)轉(zhuǎn)向了函數(shù)式編程(FP)。

函數(shù)式編程

初學(xué)者可能分辨不清 OOP 和 FP 的區(qū)別寞宫。大家注意看 onMountedwatchEffect 方法的參數(shù)——箭頭函數(shù)萧福,大致能體會(huì)到不同之處了。

OOP 的特點(diǎn)是:對(duì)象(或 class)是數(shù)據(jù)(variable)和邏輯(methods)的封裝辈赋。在 Vue2 時(shí)代鲫忍,我們經(jīng)常寫(xiě)如下代碼:

// vue2
export {
  data: () => ({count: 1}),
  methods: {
    message: (prefix) => `${prefix} ${this.count}`,
  },
  watch: {
    count() {
      console.log( this.message('Count is') );
    };
  }
}

Vue2 的內(nèi)部實(shí)現(xiàn)比較復(fù)雜,不過(guò)對(duì)外表現(xiàn)的編程模式基本就是:對(duì)象調(diào)用自己的數(shù)據(jù)和方法——this + . 操作钥屈。所以在 Vue2 時(shí)代悟民,我們通常會(huì)把相關(guān)的數(shù)據(jù)和操作寫(xiě)在同一個(gè)對(duì)象里。但是到了 Vue3 的 setup 里篷就,你幾乎不會(huì)用到 this 了射亏;變成了讓函數(shù)來(lái)調(diào)用對(duì)象或是另一個(gè)函數(shù)——就是 FP 的特點(diǎn)了。

// Vue3
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(1);
    const message = (prefix) => `${prefix} ${count.value}`;

    watchEffect(() => {
      console.log(message("Count is"));
    });
    return { count, message };
  },
};

純函數(shù)和負(fù)作用

本文不想過(guò)多介紹函數(shù)式編程,但是既然 Vue3 的風(fēng)格轉(zhuǎn)向了 FP鸦泳,我們得遵守 FP 的規(guī)則——函數(shù)只應(yīng)該做一件事银锻,就是返回一個(gè)值永品。下面的一個(gè) vue 組件就可以看做一個(gè)函數(shù)做鹰,通過(guò) props 傳入一個(gè)參數(shù) name,返回一個(gè) html鼎姐。

<template>
  <h1>{{ name }}</h1>
</template>

<script>
  export default {
    props: {
      name: String,
    },
  };
</script>

上面這個(gè)函數(shù)有什么特點(diǎn)呢钾麸?

  1. 相同的輸入產(chǎn)生相同的輸出
  2. 不能有語(yǔ)義上可觀察的函數(shù)副作用

這個(gè)就是經(jīng)典的純函數(shù)(pure function)。

pure v.s. impure

不過(guò)現(xiàn)實(shí)中一個(gè) Vue 組件可能還要做其他很多事炕桨,如:

  • 獲取數(shù)據(jù)
  • 事件監(jiān)聽(tīng)或訂閱
  • 改變應(yīng)用狀態(tài)
  • 修改 DOM
  • 輸出日志

這些其他改變就是所謂的副作用(side effect)饭尝。在 FP 的世界里,我們不能向 Vue2 那樣簡(jiǎn)單地調(diào)用全局插件了(this.$t献宫、this.$router钥平、 this.$store……);而是通過(guò)間接的手段——即通過(guò)其他函數(shù)調(diào)用——包含副作用姊途。Vue3 就提供了一個(gè)通用的副作用鉤子(hook)叫做 watchEffect(從名字上也可見(jiàn)一斑)涉瘾,就是我們今天的主角了。

watchEffect

兜兜轉(zhuǎn)轉(zhuǎn)捷兰,我們?cè)賮?lái)介紹一下 watchEffect 的用法立叛,借助 typescript,我們可以很清晰地看到該函數(shù)的定義:

類(lèi)型定義

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle;

interface WatchEffectOptions {
  flush?: "pre" | "post" | "sync";
  onTrack?: (event: DebuggerEvent) => void;
  onTrigger?: (event: DebuggerEvent) => void;
}

interface DebuggerEvent {
  effect: ReactiveEffect;
  target: any;
  type: OperationTypes;
  key: string | symbol | undefined;
}

type InvalidateCbRegistrator = (invalidate: () => void) => void;

type StopHandle = () => void;

第一個(gè)參數(shù)

watchEffect 自己是函數(shù)贡茅,它的第一個(gè)參數(shù)——effect——也是函數(shù)(函數(shù)是一等公民秘蛇,可以用在各個(gè)地方)。effect顶考,顧名思義赁还,就是包含副作用的函數(shù)。如下代碼中驹沿,副作用函數(shù)的作用是:當(dāng) count 被訪問(wèn)時(shí)艘策,旋即在控制臺(tái)打出日志。

// Vue3
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(0);
    const effect = () => console.log(count.value);
    watchEffect(effect);

    setTimeout(() => count.value++, 1000);

    return { count };
  },
};

如上代碼會(huì)打印出01甚负,0是出于 Vue 響應(yīng)式設(shè)計(jì)柬焕,在響應(yīng)式元素(count)依賴收集階段會(huì)運(yùn)行一次 effect 函數(shù);1是來(lái)自 setTimeout 里對(duì) count 加一操作梭域。

清除副作用(onInvalidate )

大家注意到?jīng)]有斑举?watchEffect 的第一個(gè)參數(shù)——effect函數(shù)——自己也有參數(shù):叫onInvalidate,也是一個(gè)函數(shù)病涨,用于清除 effect 產(chǎn)生的副作用富玷。(而且 onInvalidate 的參數(shù)也是函數(shù),哈哈!)

*p.s. FP 就是這樣赎懦,函數(shù)嵌套函數(shù)雀鹃;初學(xué)者可能有點(diǎn)暈,習(xí)慣就好*

onInvalidate 被調(diào)用的時(shí)機(jī)很微妙:它只作用于異步函數(shù)励两,并且只有在如下兩種情況下才會(huì)被調(diào)用:

  1. 當(dāng) effect 函數(shù)被重新調(diào)用時(shí)
  2. 當(dāng)監(jiān)聽(tīng)器被注銷(xiāo)時(shí)(如組件被卸載了)

如下代碼中黎茎,onInvalidate 會(huì)在 id 改變時(shí)或停止偵聽(tīng)時(shí),取消之前的異步操作(asyncOperation):

import { asyncOperation } from "./asyncOperation";

const id = ref(0);

watchEffect((onInvalidate) => {
  const token = asyncOperation(id.value);

  onInvalidate(() => {
    // run if id has changed or watcher is stopped
    token.cancel();
  });
});

返回值(停止偵聽(tīng))

副作用是隨著組件加載而發(fā)生的当悔,那么組件卸載時(shí)傅瞻,就需要清理這些副作用。watchEffect 的返回值——StopHandle依舊是一個(gè)函數(shù)——就是用在這個(gè)時(shí)候盲憎。如下 stopHandle 可以在 setup 函數(shù)里顯式調(diào)用嗅骄,也可以在組件被卸載時(shí)隱式調(diào)用。

setup() {
  const stopHandle = watchEffect(() => {
    /* ... */
  });

  // 之后
  stopHandle();
}

第二個(gè)參數(shù)

watchEffect 還有第二個(gè)參數(shù)叫 options饼疙,類(lèi)型是WatchEffectOptions溺森,一個(gè)很復(fù)雜的接口。雖然很少能被用到吧窑眯,但也在這里快速提一下屏积。

第二個(gè)參數(shù)的主要作用是指定調(diào)度器,即何時(shí)運(yùn)行副作用函數(shù)伸但。比如肾请,你希望副作用函數(shù)在組件更新前發(fā)生,可以將 flush 設(shè)為 'pre'(默認(rèn)是 'post')更胖。還有 WatchEffectOptions 也可以用于 debug:onTrackonTrigger 選項(xiàng)可用于調(diào)試一個(gè)偵聽(tīng)器的行為(當(dāng)然只開(kāi)發(fā)階段有效)铛铁。

// fire before component updates
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: "pre",
    onTrigger(e) {
      debugger;
    },
  }
);

注意點(diǎn)

watchEffect 會(huì)在 Vue3 開(kāi)發(fā)中大量使用,這里說(shuō)幾個(gè)注意點(diǎn):

  1. 如果有多個(gè)負(fù)效應(yīng)却妨,不要粘合在一起饵逐,建議寫(xiě)多個(gè) watchEffect

    watchEffect(() => {
      setTimeout(() => console.log(a.val + 1), 1000);
      setTimeout(() => console.log(b.val + 1), 1000);
    });
    

    這兩個(gè) setTimeout 是兩個(gè)不相關(guān)的效應(yīng)彪标,不需要同時(shí)監(jiān)聽(tīng) a 和 b倍权,分開(kāi)寫(xiě)吧:

    watchEffect(() => {
      setTimeout(() => console.log(a.val + 1), 1000);
    });
    
    watchEffect(() => {
      setTimeout(() => console.log(b.val + 1), 1000);
    });
    
  2. watchEffect 也可以放在其他生命周期函數(shù)內(nèi)

    比如你的副作用函數(shù)在首次執(zhí)行時(shí)就要調(diào)用 DOM,你可以把他放在 onMounted 鉤子里:

    onMounted(() => {
      watchEffect(() => {
        // access the DOM or template refs
      });
    }
    

小結(jié)

watchEffect 基本上是現(xiàn)象級(jí)拷貝了 React 的 useEffect捞烟;這里倒不是 diss Vue3薄声,只是說(shuō) watchEffect 和 useEffect 的設(shè)計(jì)都源自于一個(gè)比較成熟的編程范式——FP。大家在看 Vue3 文檔時(shí)题画,也不要只盯著某些 api 的用法默辨,Vue 只是工具,解決問(wèn)題才是終極目標(biāo)苍息;我們還是要把重點(diǎn)放在領(lǐng)悟框架的設(shè)計(jì)思想上缩幸;悟到了壹置,才是真正掌握了解決問(wèn)題的手段。最后以獨(dú)孤求敗的一句名人名言結(jié)尾:

重劍無(wú)鋒表谊,大巧不工钞护,四十歲前持之橫行天下;四十歲后爆办,不滯于物难咕,草木竹石均可為劍。

文章同步自an-Onion 的 Github押逼。碼字不易步藕,歡迎點(diǎn)贊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挑格,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沾歪,更是在濱河造成了極大的恐慌漂彤,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灾搏,死亡現(xiàn)場(chǎng)離奇詭異挫望,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)狂窑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)媳板,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人泉哈,你說(shuō)我怎么就攤上這事蛉幸。” “怎么了丛晦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵奕纫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我烫沙,道長(zhǎng)匹层,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任锌蓄,我火速辦了婚禮升筏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘸爽。我一直安慰自己您访,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布蝶糯。 她就那樣靜靜地躺著洋只,像睡著了一般辆沦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上识虚,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天肢扯,我揣著相機(jī)與錄音,去河邊找鬼担锤。 笑死蔚晨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肛循。 我是一名探鬼主播铭腕,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼多糠!你這毒婦竟也來(lái)了累舷?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤夹孔,失蹤者是張志新(化名)和其女友劉穎被盈,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體搭伤,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡只怎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怜俐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片身堡。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拍鲤,靈堂內(nèi)的尸體忽然破棺而出贴谎,到底是詐尸還是另有隱情,我是刑警寧澤殿漠,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布赴精,位于F島的核電站,受9級(jí)特大地震影響绞幌,放射性物質(zhì)發(fā)生泄漏蕾哟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一莲蜘、第九天 我趴在偏房一處隱蔽的房頂上張望谭确。 院中可真熱鬧,春花似錦票渠、人聲如沸逐哈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昂秃。三九已至禀梳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肠骆,已是汗流浹背算途。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚀腿,地道東北人嘴瓤。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像莉钙,于是被迫代替她去往敵國(guó)和親廓脆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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