最近在看 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ū)別寞宫。大家注意看 onMounted
和 watchEffect
方法的參數(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)呢钾麸?
- 相同的輸入產(chǎn)生相同的輸出
- 不能有語(yǔ)義上可觀察的函數(shù)副作用
這個(gè)就是經(jīng)典的純函數(shù)(pure function)。
不過(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ì)打印出0
和1
甚负,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)用:
- 當(dāng)
effect
函數(shù)被重新調(diào)用時(shí) - 當(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:onTrack
和 onTrigger
選項(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):
-
如果有多個(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); });
-
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)贊。