Vue.js實(shí)現(xiàn)watch屬性的示例詳解
1.寫(xiě)在前面
在上篇文章中,我們討論了compted的實(shí)現(xiàn)原理,就是利用effect和options參數(shù)進(jìn)行封裝。同樣的,watch也是基于此進(jìn)行封裝的,當(dāng)然watch還可以傳遞第三參數(shù)去清理過(guò)期的副作用函數(shù)。不僅可以利用副作用函數(shù)的調(diào)度性,去實(shí)現(xiàn)回調(diào)函數(shù)的立即執(zhí)行,也可以控制回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)。
2.watch的實(shí)現(xiàn)原理
watch本質(zhì)就是去觀測(cè)一個(gè)響應(yīng)式數(shù)據(jù),當(dāng)數(shù)據(jù)變化時(shí)通知并執(zhí)行相應(yīng)的回調(diào)函數(shù)。watch的實(shí)現(xiàn)本質(zhì)和computed類似,基于effect函數(shù)和options.scheduler選項(xiàng)。
const data = {
name:"pingping",
age:18,
flag:true
};
const state = new Proxy(data,{
/*...*/
})
watch(state,()=>{
console.log("數(shù)據(jù)變化了呀...");
});
//在響應(yīng)數(shù)據(jù)的age值被修改了,就會(huì)導(dǎo)致回調(diào)函數(shù)執(zhí)行
state.age++;watch函數(shù)的實(shí)現(xiàn)代碼如下所示,副作用函數(shù)存在scheduler選項(xiàng)中,當(dāng)響應(yīng)數(shù)據(jù)發(fā)生變化時(shí),會(huì)觸發(fā)scheduler調(diào)度函數(shù)執(zhí)行,而非直接觸發(fā)副作用函數(shù)執(zhí)行。
//watch 函數(shù)接收source響應(yīng)數(shù)據(jù)和回調(diào)函數(shù)cb
function watch(source, cb){
effect(
//觸發(fā)讀取操作,建立聯(lián)系
()=>source.age,
{
scheduler(){
// 當(dāng)數(shù)據(jù)發(fā)生變化,調(diào)用回調(diào)函數(shù)cb
cb();
}
}
)
}上面代碼片段中,我們?cè)谑褂脀atch對(duì)數(shù)據(jù)進(jìn)行監(jiān)聽(tīng)時(shí),只能對(duì)soure.age的變化進(jìn)行觀測(cè),不具有通用性,對(duì)此需要進(jìn)行進(jìn)一步封裝。
function watch(source, cb){
effect(
//調(diào)用traverse函數(shù)遞歸讀取操作,建立聯(lián)系
()=>traverse(source),
{
scheduler(){
// 當(dāng)數(shù)據(jù)發(fā)生變化,調(diào)用回調(diào)函數(shù)cb
cb();
}
}
)
}
const isObject = (value:any) => typeof value === "object" && value !== null;
function traverse(value, seen = new Set()){
//如果讀取的數(shù)據(jù)是原始值,或已經(jīng)讀取過(guò)響應(yīng)數(shù)據(jù),則什么也不做
if(!isObject(value) || seen.has(value)) return;
//將數(shù)據(jù)添加到seen中,表示遍歷讀取過(guò)數(shù)據(jù),避免循環(huán)引用導(dǎo)致死循環(huán)
seen.add(value);
//對(duì)數(shù)據(jù)對(duì)象進(jìn)行遍歷遞歸讀取,用于依賴收集
for(const k in value){
traverse(value[k],seen);
}
return value;
}在上面代碼中,單獨(dú)封裝了一個(gè)遞歸函數(shù)traverse可以對(duì)響應(yīng)數(shù)據(jù)進(jìn)行遍歷遞歸讀取操作,這樣就可以讀取到對(duì)象的上所有屬性,從而監(jiān)聽(tīng)任意屬性值發(fā)生變化時(shí)都能夠觸發(fā)回調(diào)函數(shù)。
事實(shí)上,使用watch進(jìn)行數(shù)據(jù)觀測(cè)時(shí),不僅可以觀測(cè)響應(yīng)數(shù)據(jù),還可以觀測(cè)getter函數(shù)。那么,我們只需要先對(duì)輸入的被觀測(cè)數(shù)據(jù)判斷數(shù)據(jù)類型是否為function,如果是則賦值給getter,否則還是監(jiān)聽(tīng)響應(yīng)式數(shù)據(jù)。
function watch(source,cb){
let getter;
if(typeof source === "function"){ // 如果是函數(shù)表示是getter,可以直接賦值
getter = source;
}else{
getter = () => traverse(source)// 包裝成effect對(duì)應(yīng)的effectFn, 函數(shù)內(nèi)部進(jìn)行遍歷達(dá)到依賴收集的目的
}
let oldValue, newValue;
const effectFn = effect(
()=>getter(),
{
//開(kāi)啟lazy選項(xiàng),將返回值存儲(chǔ)到effectFn中以便于之后手動(dòng)調(diào)用
lazy: true,
scheduler(){
newValue = effectFn(); // 值變化時(shí)再次運(yùn)行effect函數(shù),獲取新值
cb(newValue,oldValue);
//更新舊值,不然下次得到的是錯(cuò)誤的舊值
oldValue = newValue;
}
}
)
//手動(dòng)調(diào)用副作用函數(shù),拿到的值是舊值
oldValue = effectFn();
}其實(shí),上面代碼中充分了利用了lazy選項(xiàng)的特性,利用其創(chuàng)建一個(gè)懶執(zhí)行的effect。通過(guò)手動(dòng)執(zhí)行effectFn函數(shù)得到的返回值是舊值,當(dāng)數(shù)據(jù)變化并觸發(fā)scheduler調(diào)度器執(zhí)行時(shí),會(huì)重新執(zhí)行effectFn函數(shù)并且得到新值。
這樣,我們獲取到了數(shù)據(jù)變化前后的新值和舊值,可以將其作為參數(shù)傳遞給回調(diào)函數(shù)cb,在變化執(zhí)行副作用函數(shù)后需要將新值賦值給oldValue,方便后續(xù)執(zhí)行計(jì)算,否則后續(xù)變更會(huì)獲取到錯(cuò)誤的舊值。
寫(xiě)個(gè)demo使用下:
watch(
()=>state.age,
(newValue, oldValue)=>{
console.log(newValue, oldValue);
}
)
state.age++3.立即執(zhí)行的watch與回調(diào)執(zhí)行時(shí)機(jī)
watch本質(zhì)上對(duì)effect的二次封裝,其具有兩個(gè)特性:立即執(zhí)行的回調(diào)函數(shù)、回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)。
立即執(zhí)行的回調(diào)函數(shù)
立即執(zhí)行的回調(diào)函數(shù),默認(rèn)情況下,一個(gè)watch的回調(diào)函數(shù)只會(huì)在響應(yīng)數(shù)據(jù)發(fā)生變化時(shí)才執(zhí)行,但是在Vue.js中可以通過(guò)options.immediate來(lái)指定回調(diào)是否立即執(zhí)行。
當(dāng)options.immediate存在且為true時(shí),回調(diào)函數(shù)在該watch創(chuàng)建時(shí)立即執(zhí)行一次。事實(shí)上,回調(diào)函數(shù)的立即執(zhí)行和后續(xù)執(zhí)行在本質(zhì)上區(qū)別不大,對(duì)此可以將其調(diào)度器scheduler進(jìn)行封裝為通用函數(shù),通過(guò)options.immediate的存在與否判斷是在初始化還是變更時(shí)進(jìn)行執(zhí)行。
function watch(source, cb, options={}){
let getter;
if(type === "function"){
getter = source;
}else{
getter = ()=>traverse(source);
}
let oldValue, newValue;
// 提取調(diào)度函數(shù)為獨(dú)立的函數(shù)
const scheduler = ()=>{
newValue = effectFn(); // 值變化時(shí)再次運(yùn)行effect函數(shù),獲取新值
cb(newValue,oldValue);
//更新舊值,不然下次得到的是錯(cuò)誤的舊值
oldValue = newValue;
}
const effectFn = effect(
()=>getter(),
{
//開(kāi)啟lazy選項(xiàng),將返回值存儲(chǔ)到effectFn中以便于之后手動(dòng)調(diào)用
lazy: true,
scheduler: scheduler
}
)
if(options.immediate){
//當(dāng)immediate為true時(shí),立即執(zhí)行scheduler函數(shù)從而觸發(fā)回調(diào)執(zhí)行
scheduler()
}else{
//手動(dòng)調(diào)用副作用函數(shù),拿到的值是舊值
oldValue = effectFn();
}
}上面代碼中,回調(diào)函數(shù)是立即執(zhí)行的,在第一次回調(diào)函數(shù)執(zhí)行時(shí)沒(méi)有所謂的舊值,此時(shí)回調(diào)函數(shù)的oldValue值是undefined。
回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)
當(dāng)然,除了上面的可以指定回調(diào)函數(shù)為立即執(zhí)行外,還可以通過(guò)options參數(shù)來(lái)指定回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)。在Vue.js3中可以通過(guò)flush選項(xiàng)來(lái)指定調(diào)度函數(shù)的執(zhí)行時(shí)機(jī),當(dāng)flush的值為"post"時(shí),表示調(diào)度函數(shù)需要將副作用函數(shù)放在微任務(wù)隊(duì)列中,等待DOM更新結(jié)束后執(zhí)行。
function watch(source, cb, options={}){
let getter;
if(type === "function"){
getter = source;
}else{
getter = ()=>traverse(source);
}
let oldValue, newValue;
// 提取調(diào)度函數(shù)為獨(dú)立的函數(shù)
const obj = ()=>{
newValue = effectFn(); // 值變化時(shí)再次運(yùn)行effect函數(shù),獲取新值
cb(newValue,oldValue);
//更新舊值,不然下次得到的是錯(cuò)誤的舊值
oldValue = newValue;
}
const effectFn = effect(
()=>getter(),
{
//開(kāi)啟lazy選項(xiàng),將返回值存儲(chǔ)到effectFn中以便于之后手動(dòng)調(diào)用
lazy: true,
scheduler(){
if(options.flush === "post"){
const p = Promise.resolve();
p.then(obj);
}else{
obj();
}
}
}
)
if(options.immediate){
//當(dāng)immediate為true時(shí),立即執(zhí)行scheduler函數(shù)從而觸發(fā)回調(diào)執(zhí)行
scheduler()
}else{
//手動(dòng)調(diào)用副作用函數(shù),拿到的值是舊值
oldValue = effectFn();
}
}其實(shí)就是根據(jù)options.flush是否等于"post",來(lái)實(shí)現(xiàn)是否需要將obj函數(shù)進(jìn)行異步處理。
4.過(guò)期的副作用函數(shù)和cleanup
在講到watch過(guò)期的副作用函數(shù),就要提到在多進(jìn)程或多線程編程中經(jīng)常被提及的競(jìng)態(tài)問(wèn)題。在下面代碼片段中,使用watch觀測(cè)state對(duì)象的變化,每次state對(duì)象發(fā)生變化時(shí)都會(huì)發(fā)送網(wǎng)絡(luò)請(qǐng)求。
let finalData;
watch(state, async ()=>{
//發(fā)送等待網(wǎng)絡(luò)請(qǐng)求
const res = await fetch("/user/info");
finalData = res;
})在上面代碼看起來(lái)是沒(méi)啥問(wèn)題,但其實(shí)會(huì)發(fā)生競(jìng)態(tài)問(wèn)題,在第一次修改state對(duì)象的字段值后,會(huì)導(dǎo)致回調(diào)執(zhí)行,同時(shí)發(fā)送第一次請(qǐng)求A;在A請(qǐng)求返回結(jié)果之前,我們繼續(xù)修改state的字段值,同時(shí)發(fā)送第二次請(qǐng)求B。但是請(qǐng)求A和請(qǐng)求B誰(shuí)的結(jié)果先返回,我們并不知道?

將A的請(qǐng)求結(jié)果覆蓋B的請(qǐng)求結(jié)果
在理論分析下,我們先后發(fā)送A、B請(qǐng)求,按道理應(yīng)該是先返回A,再返回B請(qǐng)求的結(jié)果。這是因?yàn)檎?qǐng)求A是副作用函數(shù)第一次執(zhí)行產(chǎn)生的副作用,而請(qǐng)求B是副作用函數(shù)第二次執(zhí)行產(chǎn)生的副作用。請(qǐng)求B在請(qǐng)求A后發(fā)生,請(qǐng)求A應(yīng)當(dāng)在這之前就過(guò)期了,返回的結(jié)果應(yīng)該是無(wú)效的。
但是,在前面沒(méi)有對(duì)watch的執(zhí)行時(shí)機(jī)進(jìn)行調(diào)度的情況下,就會(huì)發(fā)生請(qǐng)求A的值后返回覆蓋B請(qǐng)求返回值的錯(cuò)誤。
要想解決這種問(wèn)題,我們只需要提供一個(gè)副作用過(guò)期的手段即可。事實(shí)上,watch函數(shù)的回調(diào)函數(shù)可以傳入第三個(gè)參數(shù)onInvalidate函數(shù),讓其注冊(cè)一個(gè)回調(diào)在當(dāng)前副作用函數(shù)過(guò)期時(shí)執(zhí)行:
function watch(source, cb, options={}){
let getter;
if(type === "function"){
getter = source;
}else{
getter = ()=>traverse(source);
}
let oldValue, newValue;
let cleanupFn;//用于存儲(chǔ)用戶注冊(cè)的過(guò)期回調(diào)
//定義onInvalidate函數(shù)
const onInvalidate = (fn)=>{
//將過(guò)期的回調(diào)函數(shù)存儲(chǔ)到cleanupFn中
fn();
}
// 提取調(diào)度函數(shù)為獨(dú)立的函數(shù)
const obj = ()=>{
newValue = effectFn(); // 值變化時(shí)再次運(yùn)行effect函數(shù),獲取新值
//在調(diào)用回調(diào)函數(shù)cb之前,先調(diào)用過(guò)期的回調(diào)函數(shù)
if(cleanupFn){
cleanupFn();
}
cb(newValue,oldValue,onInvalidate);
//更新舊值,不然下次得到的是錯(cuò)誤的舊值
oldValue = newValue;
}
const effectFn = effect(
()=>getter(),
{
//開(kāi)啟lazy選項(xiàng),將返回值存儲(chǔ)到effectFn中以便于之后手動(dòng)調(diào)用
lazy: true,
scheduler(){
if(options.flush === "post"){
const p = Promise.resolve();
p.then(obj);
}else{
obj();
}
}
}
)
if(options.immediate){
//當(dāng)immediate為true時(shí),立即執(zhí)行scheduler函數(shù)從而觸發(fā)回調(diào)執(zhí)行
scheduler()
}else{
//手動(dòng)調(diào)用副作用函數(shù),拿到的值是舊值
oldValue = effectFn();
}
}在上面代碼片段中,定義變量存儲(chǔ)用戶通過(guò)onInvalidate函數(shù)注冊(cè)的回調(diào)函數(shù),將過(guò)期的回調(diào)賦值給cleanupFn,在job函數(shù)中每次執(zhí)行回調(diào)函數(shù)cb前都會(huì)檢查是否存在過(guò)期回調(diào)。存在過(guò)期回調(diào)則執(zhí)行cleanupFn函數(shù)清理,最后將onInvalidate返回給用戶使用。
寫(xiě)個(gè)demo實(shí)踐下:
watch(state, async (newValue, oldValue, onInvalidate)=>{
let expired = false;
onInvalidate(()=>{
expired = true;
})
const res = await fetch("/user/info");
if(!expired){
finaleData = res;
}
});
//第一次修改
state.age++;
setTimeout(()=>{
state.age++;
},200)示意圖如下:

請(qǐng)求過(guò)期
5.寫(xiě)在最后
本文中,討論了watch函數(shù)是如何利用副作用函數(shù)和options進(jìn)行封裝實(shí)現(xiàn)的,也通過(guò)調(diào)度函數(shù)去控制回調(diào)函數(shù)的立即執(zhí)行和執(zhí)行時(shí)機(jī),還可以解決競(jìng)態(tài)問(wèn)題。
以上就是Vue.js實(shí)現(xiàn)watch屬性的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue.js watch屬性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- vue中的watch監(jiān)聽(tīng)數(shù)據(jù)變化及watch中各屬性的詳解
- 詳解Vue中watch對(duì)象內(nèi)屬性的方法
- Vue使用watch監(jiān)聽(tīng)一個(gè)對(duì)象中的屬性的實(shí)現(xiàn)方法
- Vue.js watch監(jiān)視屬性知識(shí)點(diǎn)總結(jié)
- Vue中的?watch監(jiān)聽(tīng)屬性詳情
- vue正確使用watch監(jiān)聽(tīng)屬性變化方式
- 淺析vue3響應(yīng)式數(shù)據(jù)與watch屬性
- 使用Vue逐步實(shí)現(xiàn)Watch屬性詳解
相關(guān)文章
vue + el-form 實(shí)現(xiàn)的多層循環(huán)表單驗(yàn)證
這篇文章主要介紹了vue + el-form 實(shí)現(xiàn)的多層循環(huán)表單驗(yàn)證,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下。2020-11-11
vite+vue3項(xiàng)目集成ESLint與prettier的過(guò)程詳解
這篇文章主要介紹了vite+vue3項(xiàng)目中集成ESLint與prettier的相關(guān)知識(shí),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
vue router返回到指定的路由的場(chǎng)景分析
這篇文章主要介紹了vue router返回到指定的路由的場(chǎng)景分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
Element?Plus在el-form-item中設(shè)置justify-content無(wú)效的解決方案
這篇文章主要介紹了Element?Plus在el-form-item中設(shè)置justify-content無(wú)效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
vue子元素綁定的事件, 阻止觸發(fā)父級(jí)上的事件處理方式
這篇文章主要介紹了vue子元素綁定的事件, 阻止觸發(fā)父級(jí)上的事件處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
vue中img或元素背景圖片無(wú)法顯示或路徑錯(cuò)誤的解決
這篇文章主要介紹了vue中img或元素背景圖片無(wú)法顯示或路徑錯(cuò)誤的解決方案,具有很好的參考價(jià)值。希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Vue進(jìn)階之利用transition標(biāo)簽實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)動(dòng)畫(huà)
這篇文章主要為大家詳細(xì)介紹了Vue如何利用transition標(biāo)簽實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)動(dòng)畫(huà),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起一下2023-08-08

