watchEffect
執(zhí)行監(jiān)聽
watchEffect比較奇特,它跟Vue 2的watch有所區(qū)別,它的寫法是:
watchEffect(() => {
// 執(zhí)行一些操作,其中必須含有響應(yīng)式變量
})
為什么感覺怪怪的?watchEffect并沒有要求你聲明被監(jiān)聽的變量,而是,你在執(zhí)行體里寫哪個(gè)變量,Vue就收集、監(jiān)聽哪個(gè)變量,而且可以同時(shí)監(jiān)聽多個(gè)變量,看下例:
<template>
<div>
<button @click="r++">{{ r }}</button>
<button @click="s.a++">{{ s.a }}</button>
<button @click="s.b++">{{ s.b }}</button>
<button @click="s.a++;s.b++">{{ s.a }} - {{ s.b }}</button>
</div>
</template>
<script>
import { ref, computed, watchEffect } from 'vue';
export default {
setup() {
let r = ref(10);
watchEffect(() => {
console.log(r.value);
});
let s = ref({a: 100, b: 200});
watchEffect(() => {
console.log('a:', s.value.a);
});
watchEffect(() => {
console.log('b:', s.value.b);
});
watchEffect(() => {
console.log('a - b:', s.value.a + '-' + s.value.b);
});
watchEffect(() => {
console.log('value:', s.value);
});
return {
r,s
};
},
};
</script>
可以看到:
首先,watchEffect是立即執(zhí)行的,所以組件初始化的時(shí)候就全部執(zhí)行了一遍。
點(diǎn)擊button1,打印10,很好理解。
s的傳入值是個(gè)對(duì)象,button2修改的是屬性a,那么,只有監(jiān)聽屬性a的監(jiān)聽器才會(huì)有反應(yīng),只跟屬性b相關(guān)的監(jiān)聽是不會(huì)有反應(yīng)的,只監(jiān)聽s.value的監(jiān)聽器也不會(huì)有反應(yīng)。點(diǎn)擊button3和button4也會(huì)印證這個(gè)結(jié)論。
在watchEffect里操作響應(yīng)式數(shù)據(jù),不會(huì)引起無限循環(huán)監(jiān)聽,這雖然很顯而易見,但是也在此說一句。
多個(gè)watchEffect的執(zhí)行順序是watchEffect的書寫順序。
watchEffect拿不到更新前的值,這一點(diǎn)要注意。
停止監(jiān)聽
- 自動(dòng)停止
先說watchEffect生命周期的開始,是從組件的setup()函數(shù)或生命周期鉤子被調(diào)用時(shí)開始。自動(dòng)停止是在組件卸載時(shí)自動(dòng)停止。
- 手動(dòng)停止
將watchEffect賦值給變量,執(zhí)行這個(gè)變量即可手動(dòng)停止。比如:
const xx = watchEffect(() => {
console.log('a:', s.value.a);
s.value.a += 10
});
// 后來某個(gè)時(shí)間執(zhí)行了:
xx(); // 停止監(jiān)聽
清除副作用
官方文檔:https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html#清除副作用
官方文檔里偶爾會(huì)蹦出來一個(gè)詞“副作用”,初學(xué)者看完一頭霧水,什么鬼副作用?英文文檔里副作用是Side Effect,到底什么意思?
純函數(shù)里的副作用概念
副作用其實(shí)是一個(gè)比較生僻的概念,最早來自于“純函數(shù)”,純函數(shù)是編程界早期的一個(gè)概念,具體可以看(https://zhuanlan.zhihu.com/p/139659155 和 https://juejin.cn/post/6950059795659358221)。純函數(shù)特征:
它應(yīng)始終返回相同的值。不管調(diào)用該函數(shù)多少次,無論今天、明天還是將來某個(gè)時(shí)候調(diào)用它。
自包含(不使用全局變量)。
它不應(yīng)修改程序的狀態(tài)或引起副作用(修改全局變量)。
注意看,這里就出現(xiàn)了“副作用”。所以,純函數(shù)的副作用就是:
一個(gè)函數(shù)除了返回確定的值之外,還做了其他的事情,那么這個(gè)函數(shù)做的這些事情就叫做“副作用”。
比如console.log(123)
就有副作用,它返回undefined
是主作用,但是我們不需要它的主作用,它的副作用就是在控制臺(tái)打印123
,我們要的是它的副作用。
再比如:
let counter = 0;
// 有副作用,副作用是把外部變量改了
incCounter() {
counter += 1;
return counter;
}
// 沒有副作用
incNumber(m) {
return m + 1;
}
React中的副作用概念
React等框架早先就在使用這個(gè)詞,Vue從3.0開始,在文檔里出現(xiàn)這個(gè)詞。
那么,在React中,是不是副作用也是這個(gè)定義呢?未必??纯碦eact是怎么說的:
你之前可能已經(jīng)在 React 組件中執(zhí)行過數(shù)據(jù)獲取、訂閱或者手動(dòng)修改過 DOM。我們統(tǒng)一把這些操作稱為“副作用”,或者簡(jiǎn)稱為“作用”。
useEffect
就是一個(gè) Effect Hook,給函數(shù)組件增加了操作副作用的能力。它跟 class 組件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不過被合并成了一個(gè) API。
例如,下面這個(gè)組件在 React 更新 DOM 后會(huì)設(shè)置一個(gè)頁(yè)面標(biāo)題:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相當(dāng)于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用瀏覽器的 API 更新頁(yè)面標(biāo)題
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
當(dāng)你調(diào)用
useEffect
時(shí),就是在告訴 React 在完成對(duì) DOM 的更改后運(yùn)行你的“副作用”函數(shù)。由于副作用函數(shù)是在組件內(nèi)聲明的,所以它們可以訪問到組件的 props 和 state。默認(rèn)情況下,React 會(huì)在每次渲染后調(diào)用副作用函數(shù) —— 包括第一次渲染的時(shí)候。
副作用函數(shù)還可以通過返回一個(gè)函數(shù)來指定如何“清除”副作用。
看完了之后,我們就知道React對(duì)于副作用的解釋:
在執(zhí)行所有業(yè)務(wù)邏輯之前,也就是組件初始化的時(shí)候,組件根據(jù)開發(fā)者的設(shè)定,由自身驅(qū)動(dòng)的第一次DOM修改,就是主作用。此時(shí),組件還沒有執(zhí)行任何一行邏輯代碼。
主作用之后,組件就開始執(zhí)行用戶邏輯了,這里你眼里的業(yè)務(wù)邏輯代碼,在React眼里都是副作用。
Vue 3里的副作用概念
跟React應(yīng)該是說的一個(gè)意思,具體說:
所以首先了解一下“主作用”,在Vue世界里,視圖層和DOM層是兩碼事,盡管一些初級(jí)程序員認(rèn)為它們是一碼事。變更響應(yīng)式數(shù)據(jù)的主作用就是變更后的數(shù)據(jù)能渲染到視圖層。前端還有比這個(gè)事更重要的事嗎?沒有吧。
副作用就是響應(yīng)式數(shù)據(jù)的變更造成的其他連鎖反應(yīng),以及后續(xù)邏輯,這些連鎖反應(yīng)都叫副作用。在藥物學(xué)里,副作用往往是不良反應(yīng),但是在Vue 3里并不是。上面標(biāo)題里說“清除副作用”,也并不是說因?yàn)楦弊饔檬遣涣挤磻?yīng)所以要清除,而是Vue 3提供一個(gè)方法讓你隨時(shí)可以取消副作用。
副作用主要有:
DOM更新
watchEffect
watch
computed
...
你沒看錯(cuò),既然更新視圖層才是主作用,那么視圖層更新到DOM上在Vue眼里是副作用,而且,變更響應(yīng)式數(shù)據(jù)觸發(fā)執(zhí)行computed和觸發(fā)執(zhí)行watchEffect當(dāng)然也是副作用。所以watchEffect本身就是副作用。
清除副作用是什么意思
那么官方文檔說的“清除副作用”到底在說什么?它意思是說,如果有些副作用是異步的,這就意味著你可以取消它,那么Vue創(chuàng)始人就給你提供了一個(gè)方法,讓你優(yōu)雅的取消這些異步副作用。
比如你有一個(gè)頁(yè)碼組件,里面有5個(gè)頁(yè)碼,點(diǎn)擊就會(huì)異步請(qǐng)求數(shù)據(jù)。于是我就做了一個(gè)監(jiān)聽,監(jiān)聽當(dāng)前頁(yè)碼,只要有變化就ajax一次。下例是不可直接運(yùn)行的演示代碼:
let content = '';
const pageNumber = ref(1);
function onClickPageNumber(val) {
pageNumber.value = val;
}
watchEffect(() => {
ajax({pageNumber}).then(response => {
content = response.data;
})
});
現(xiàn)在問題是,如果我點(diǎn)擊的比較快,從1到5全點(diǎn)了一遍,那么會(huì)有5個(gè)ajax請(qǐng)求,最終頁(yè)面會(huì)顯示第幾頁(yè)的內(nèi)容?你說第5頁(yè)?那你是假定請(qǐng)求第5頁(yè)的ajax響應(yīng)的最晚,事實(shí)呢?并不一定。于是這就會(huì)導(dǎo)致錯(cuò)亂。還有一個(gè)問題,我連續(xù)快速點(diǎn)5次頁(yè)碼,等于我并不想看前4頁(yè)的內(nèi)容,那么是不是前4次的請(qǐng)求都屬于帶寬浪費(fèi)?這也不好。于是官方就給出了一種解決辦法:
首先,你的異步操作必須是能中止的異步操作,對(duì)于定時(shí)器來講中止定時(shí)器很容易,clearInterval之類的就可以,但對(duì)于ajax來講,需要借助ajax庫(kù)(比如axios)提供的中止ajax辦法來中止ajax。現(xiàn)在我寫一個(gè)能直接運(yùn)行的范例演示一下中止異步操作:
我先搭建一個(gè)最簡(jiǎn)Node服務(wù)器,3300端口的:
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', "*");
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS');
res.writeHead(200, {
'Content-Type': 'application/json'
});
});
server.listen(3300, () => {
console.log('Server is running...');
});
server.on('request', (req, res) => {
setTimeout(() => {
if (/\d.json/.test(req.url)) {
const data = {
content: '我是內(nèi)容,來自' + req.url
}
res.end(JSON.stringify(data));
}
}, Math.random() * 2000);
});
清除副作用的核心有2點(diǎn):
異步副作用要給出取消自身的辦法
watchEffect提供取消副作用的接口,也就是onInvalidate方法。
Invalidate
中文譯義是作廢,onInvalidate也就是作廢監(jiān)聽器。
<template>
<div>
<div>content: {{ content }}</div>
<button @click="pageNumber = (pageNumber++ % 5) + 1">{{ pageNumber }}</button>
</div>
</template>
<script>
import axios from 'axios';
import { ref, watchEffect } from 'vue';
export default {
setup() {
let pageNumber = ref(1);
let content = ref('');
watchEffect((onInvalidate) => {
// const CancelToken = axios.CancelToken;
// const source = CancelToken.source();
// onInvalidate(() => {
// source.cancel();
// });
axios
.get(`http://localhost:3300/${pageNumber.value}.json`, {
// cancelToken: source.token,
})
.then((response) => {
content.value = response.data.content;
})
.catch(function (err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
}
});
});
return {
pageNumber,
content,
};
},
};
</script>
上面注釋掉的代碼先保持注釋掉,然后我們經(jīng)過20多次瘋狂點(diǎn)擊之后,得到這個(gè)結(jié)果,顯然,內(nèi)容錯(cuò)亂了:
現(xiàn)在我取消注釋,重新20多次瘋狂點(diǎn)擊,得到的結(jié)果就正確了:
除了最后一個(gè)請(qǐng)求,上面那些請(qǐng)求有2種結(jié)局:
一種是響應(yīng)的太快,來不及取消的請(qǐng)求,這種請(qǐng)求會(huì)返回200,不過既然它響應(yīng)太快,沒有任何一次后續(xù)ajax能夠來得及取消它,說明任何一次后續(xù)ajax開始之前,它就已經(jīng)結(jié)束了,那么它一定會(huì)被后續(xù)某些請(qǐng)求所覆蓋,所以這類請(qǐng)求的content會(huì)顯示一瞬間,然后被后續(xù)的請(qǐng)求覆蓋,絕對(duì)不會(huì)比后面的請(qǐng)求還晚。
另一種就是紅色的那些被取消的請(qǐng)求,因?yàn)轫憫?yīng)的慢,所以被取消掉了。
所以最終結(jié)果一定是正確的,而且節(jié)省了很多帶寬,也節(jié)省了系統(tǒng)開銷。
這就是官方說的“清除副作用”。清除定時(shí)器更簡(jiǎn)單,我不舉例了。
副作用刷新時(shí)機(jī)
官方文檔:https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html#副作用刷新時(shí)機(jī)
官方文檔里的“副作用刷新時(shí)機(jī)”更晦澀,我解釋一下。
Vue 的響應(yīng)性系統(tǒng)會(huì)緩存副作用函數(shù),并異步地刷新它們,這樣可以避免同一個(gè)“tick”中多個(gè)狀態(tài)改變導(dǎo)致的不必要的重復(fù)調(diào)用。
同一個(gè)“tick”的意思是,Vue的內(nèi)部機(jī)制會(huì)以最科學(xué)的計(jì)算規(guī)則將視圖刷新請(qǐng)求合并成一個(gè)一個(gè)的"tick",每個(gè)“tick”刷新一次視圖,比如a=1;b=2;
只會(huì)觸發(fā)一次視圖刷新。$nextTick的Tick就是指這個(gè)。
繼續(xù)說,比如有個(gè)watchEffect監(jiān)聽了2個(gè)變量a和b,我的業(yè)務(wù)寫了a=1;b=2;
,你覺得監(jiān)聽器會(huì)調(diào)用2次?當(dāng)然不會(huì),Vue會(huì)合并成1次去執(zhí)行,代碼如下,console.log只會(huì)執(zhí)行一次:
<template>
<div>
<button
@click="
r++;
s++;
"
>
{{ r }} - {{ s }}
</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
let r = ref(2);
let s = ref(10);
watchEffect(() => {
console.log(r.value, s.value);
});
return {
r,
s,
};
},
};
</script>
在核心的具體實(shí)現(xiàn)中,組件的
update
函數(shù)也是一個(gè)被偵聽的副作用。當(dāng)一個(gè)用戶定義的副作用函數(shù)進(jìn)入隊(duì)列時(shí),默認(rèn)情況下,會(huì)在所有的組件update
前執(zhí)行。
所謂組件的update
函數(shù)是Vue內(nèi)置的用來更新DOM的函數(shù),它也是副作用,上文已經(jīng)提到過。這時(shí)候有一個(gè)問題,就是默認(rèn)下,Vue會(huì)先執(zhí)行組件DOM update,還是先執(zhí)行監(jiān)聽器?測(cè)一下:
<template>
<div>
<button
id="aa"
@click="
r++;
s++;
"
>
{{ r }} - {{ s }}
</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
let r = ref(2);
let s = ref(10);
watchEffect(
() => {
console.log(r.value, s.value);
console.log(document.querySelector('#aa') && document.querySelector('#aa').innerText);
}
);
return {
r,
s,
};
},
};
</script>
點(diǎn)擊若干次(比如2次)按鈕,得到的結(jié)果是:
為什么點(diǎn)之前按鈕的innerText打印null?因?yàn)槭聦?shí)就是默認(rèn)先執(zhí)行監(jiān)聽器,然后更新DOM,此時(shí)DOM還未生成,當(dāng)然是null。
當(dāng)我第1和2次點(diǎn)擊完,你會(huì)發(fā)現(xiàn),document.querySelector('#aa').innerText
獲取到的總是點(diǎn)擊之前DOM的內(nèi)容。這也說明,默認(rèn)Vue先執(zhí)行監(jiān)聽器,所以取到了上一次的內(nèi)容,然后執(zhí)行組件update。
Vue 2其實(shí)也是這種機(jī)制,Vue 2使用this.$nextTick()去獲取組件更新完成之后的DOM,在watchEffect里就不需要用this.$nextTick()(也沒法用),有一個(gè)辦法能獲取組件更新完成之后的DOM,就是使用:
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
現(xiàn)在設(shè)上flush配置項(xiàng),重新進(jìn)入組件,再看看:
沒設(shè)flush: 'post' | 設(shè)了flush: 'post' |
---|---|
image.png
|
image.png
|
所以結(jié)論是,如果要操作“更新之后的DOM”,就要配置flush: 'post'。
watch
Vue 3 watch與Vue 2 watch對(duì)比
- Vue 3 watch與Vue 2的實(shí)例方法vm.$watch(也就是this.$watch)的基本用法差不多,只不過程序員大多使用watch配置項(xiàng),可能對(duì)$watch實(shí)例方法不太熟。實(shí)例方法的一個(gè)優(yōu)勢(shì)是更靈活,第一個(gè)參數(shù)可以接受一個(gè)函數(shù),等于是接受了一個(gè)getter函數(shù)。
<template>
<div>
<button @click="r++">{{ r }}</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let r = ref(1);
let s = ref(10);
watch(
() => r.value + s.value,
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
return {
r,
s,
};
},
};
</script>
- Vue 3 watch增加了同時(shí)監(jiān)聽多個(gè)變量的能力,用數(shù)組表達(dá)要監(jiān)聽的變量。回調(diào)參數(shù)是這種結(jié)構(gòu):
[newR, newS, newT], [oldR, oldS, oldT]
,不要理解成其他錯(cuò)誤的結(jié)構(gòu)。
<template>
<div>
<button @click="r++">{{ r }}</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let r = ref(1);
let s = ref(10);
let t = ref(100);
watch(
[r, s, t],
([newR, newS, newT], [oldR, oldS, oldT]) => {
console.log([newR, newS, newT], [oldR, oldS, oldT]);
}
);
return {
r,
};
},
};
</script>
被監(jiān)聽的變量必須是:
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.
也就是說,可以是getter/effect函數(shù)、ref、Proxy以及它們的數(shù)組。絕對(duì)不可以是純對(duì)象或基本數(shù)據(jù)。想要Vue 3的watch立即執(zhí)行,可以在watch的最后一個(gè)參數(shù)寫上
{immediate: true}
。Vue 3的深度監(jiān)聽還有沒有?當(dāng)然有,而且默認(rèn)就是,無需聲明。當(dāng)然,前提是深層property也是響應(yīng)式的。如果深層property無響應(yīng)式,那么即便寫上
{deep: true}
也沒用。
Vue 3 watch與Vue 3 watchEffect的差異
這方面官方文檔說的還可以:
- 惰性地執(zhí)行副作用,也就是說不會(huì)立即執(zhí)行一次;
- 更具體地說明應(yīng)觸發(fā)偵聽器重新運(yùn)行的狀態(tài),這句話翻譯還是很晦澀,其實(shí)意思是說,你現(xiàn)在能一眼看出來哪個(gè)變量被監(jiān)聽;
- 能訪問偵聽狀態(tài)的先前值和當(dāng)前值,不要小看這個(gè)差別,有時(shí)候拿不到先前值就沒法進(jìn)行業(yè)務(wù)。
所以,當(dāng)你不希望立即執(zhí)行一次監(jiān)聽器,或者需要拿到先前值,或者想明確表明哪些變量被監(jiān)聽了,就用watch。
其他差異有:
- 如果監(jiān)聽一個(gè)Proxy變量p,它的內(nèi)部值結(jié)構(gòu)是
{a: {b: {c: 2}}}
或{a: {b: {c: {d: 3}}}}
,我打算監(jiān)聽p.a.b.c,那么:
watchEffect | watch且p.a.b.c是基本類型 | watch且p.a.b.c是引用類型 |
---|---|---|
必須監(jiān)聽p.a.b.c自身 | 必須監(jiān)聽p.a.b.c的任意一級(jí)上級(jí)property | 監(jiān)聽p.a.b.c自身和任意上級(jí)property均可 |
- 如果監(jiān)聽ref,跟上面類似,只是有2個(gè)注意事項(xiàng):一是p后面不要忘記加.value,二是所謂“p.value.a.b.c的任意上級(jí)property”最高只允許到
p.value
,不能到p
。
Vue 3 watch與Vue 3 watchEffect的共性
官方說,watch也有停止偵聽,清除副作用、副作用刷新時(shí)機(jī)和偵聽器調(diào)試行為。簡(jiǎn)單舉例:
- watch停止監(jiān)聽:
停止監(jiān)聽watch很簡(jiǎn)單,watch的時(shí)候就必須賦值給一個(gè)變量,這時(shí)候就開始監(jiān)聽。想停止監(jiān)聽就把這個(gè)變量當(dāng)函數(shù)執(zhí)行一下。
<template>
<div>
<button @click="r++">{{ r }}</button>
<button @click="s()">stop</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let r = ref(2);
let s = watch(r, () => {
console.log(r.value);
});
return {
r,
s,
};
},
};
</script>
- watch清除副作用:
<template>
<div>
<div>content: {{ content }}</div>
<button @click="pageNumber = (pageNumber++ % 5) + 1">{{ pageNumber }}</button>
</div>
</template>
<script>
import axios from 'axios';
import { ref, watch } from 'vue';
export default {
setup() {
let pageNumber = ref(1);
let content = ref('');
watch(pageNumber, (newVal, oldVal, onInvalidate) => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
onInvalidate(() => {
source.cancel();
});
axios
.get(`http://localhost:3300/${pageNumber.value}.json`, {
cancelToken: source.token,
})
.then((response) => {
content.value = response.data.content;
})
.catch(function (err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
}
});
});
return {
pageNumber,
content,
};
},
};
</script>
- 調(diào)整副作用刷新時(shí)機(jī),可以嘗試注釋flush: 'post',作為對(duì)比:
<template>
<div>
<button
id="aa"
@click="
r++;
s++;
"
>
{{ r }} - {{ s }}
</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let r = ref(2);
let s = ref(10);
watch(r,
() => {
console.log(r.value, s.value);
console.log(document.querySelector('#aa') && document.querySelector('#aa').innerText);
},
{
flush: 'post'
}
);
return {
r,
s,
};
},
};
</script>
computed
Vue 3跟Vue 2的computed的差別在于,Vue 2是所有計(jì)算屬性都是根對(duì)象的屬性,Vue 3是計(jì)算屬性都是獨(dú)立變量,其他區(qū)別很小,就不細(xì)說了。
Vue 3 computed特點(diǎn):
computed默認(rèn)接收getter函數(shù),也可以接收一個(gè)對(duì)象,對(duì)象里有g(shù)et和set方法。set方法接收一個(gè)val參數(shù)。初學(xué)者可能會(huì)忘記寫getter函數(shù),只寫計(jì)算表達(dá)式,要注意這點(diǎn)。
computed一定返回ref對(duì)象,所以并不需要在計(jì)算函數(shù)里給返回值添加響應(yīng)式,這屬于畫蛇添足。