天天看點

Vue3中 watch、watchEffect 詳解

1. watch 的使用

文法

import { watch } from "vue" 
watch( name , ( curVal , preVal )=>{ //業務處理  }, options ) ;

共有三個參數,分别為:
  name:需要幀聽的屬性;
  (curVal,preVal)=>{ //業務處理 } 箭頭函數,是監聽到的最新值和本次修改之前的值,此處進行邏輯處理。
  options :配置項,對監聽器的配置,如:是否深度監聽。      

1.1 監聽 ref 定義的響應式資料

<template>
  <div>
    <div>值:{{count}}</div>
    <button @click="add">改變值</button>
  </div>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  setup(){
    const count = ref(0);
    const add = () => {
      count.value ++
    };
    watch(count,(newVal,oldVal) => {
      console.log('值改變了',newVal,oldVal)
    })
    return {
      count,
      add,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

1.2 監聽 reactive 定義的響應式資料

<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <button @click="changeName">改變值</button>
  </div>
</template>

<script>
import { reactive, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch(obj,(newVal,oldVal) => {
      console.log('值改變了',newVal,oldVal)
    })
    return {
      obj,
      changeName,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

1.3 監聽多個響應式資料資料

<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <div>{{count}}</div>
    <button @click="changeName">改變值</button>
  </div>
</template>

<script>
import { reactive, ref, watch } from 'vue';
export default {
  setup(){
    const count = ref(0);
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch([count,obj],() => {
      console.log('監聽的多個資料改變了')
    })
    return {
      obj,
      count,
      changeName,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

1.4 監聽對象中某個屬性的變化

<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <button @click="changeName">改變值</button>
  </div>
</template>

<script>
import { reactive, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch(() => obj.name,() => {
      console.log('監聽的obj.name改變了')
    })
    return {
      obj,
      changeName,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

1.5 深度監聽(deep)、預設執行(immediate)

<template>
  <div>
    <div>{{obj.brand.name}}</div>
    <button @click="changeBrandName">改變值</button>
  </div>
</template>

<script>
import { reactive, ref, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14,
      brand:{
        id:1,
        name:'寶馬'
      }
    });
    const changeBrandName = () => {
      obj.brand.name = '奔馳';
    };
    watch(() => obj.brand,() => {
      console.log('監聽的obj.brand.name改變了')
    },{
      deep:true,
      immediate:true,
    })
    return {
      obj,
      changeBrandName,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

2. watchEffect 的使用

watchEffect 也是一個幀聽器,是一個副作用函數。

它會監聽引用資料類型的所有屬性,不需要具體到某個屬性,一旦運作就會立即監聽,元件解除安裝的時候會停止監聽。

<template>
  <div>
    <input type="text" v-model="obj.name"> 
  </div>
</template>

<script>
import { reactive, watchEffect } from 'vue';
export default {
  setup(){
    let obj = reactive({
      name:'zs'
    });
    watchEffect(() => {
      console.log('name:',obj.name)
    })

    return {
      obj
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

停止偵聽

當 watchEffect 在元件的 setup() 函數或生命周期鈎子被調用時,偵聽器會被連結到該元件的生命周期,并在元件解除安裝時自動停止。

在一些情況下,也可以顯式調用傳回值以停止偵聽:

<template>
  <div>
    <input type="text" v-model="obj.name"> 
    <button @click="stopWatchEffect">停止監聽</button>
  </div>
</template>

<script>
import { reactive, watchEffect } from 'vue';
export default {
  setup(){
    let obj = reactive({
      name:'zs'
    });
    const stop = watchEffect(() => {
      console.log('name:',obj.name)
    })
    const stopWatchEffect = () => {
      console.log('停止監聽')
      stop();
    }

    return {
      obj,
      stopWatchEffect,
    }
  }
}
</script>      
Vue3中 watch、watchEffect 詳解

清除副作用

有時副作用函數會執行一些異步的副作用,這些響應需要在其失效時清除 (場景:有一個頁碼元件裡面有5個頁碼,點選就會異步請求資料。于是做一個監聽,監聽目前頁碼,隻要有變化就請求一次。問題:如果點選的比較快,從1到5全點了一遍,那麼會有5個請求,最終頁面會顯示第幾頁的内容?第5頁?那是假定請求第5頁的ajax響應的最晚,事實呢?并不一定。于是這就會導緻錯亂。還有一個問題,連續快速點5次頁碼,等于我并不想看前4頁的内容,那麼是不是前4次的請求都屬于帶寬浪費?這也不好。

于是官方就給出了一種解決辦法:

偵聽副作用傳入的函數可以接收一個 onInvalidate 函數作入參,用來注冊清理失效時的回調。

當以下情況發生時,這個失效回調會被觸發:

  • 副作用即将重新執行時;
  • 偵聽器被停止 (如果在 setup() 或生命周期鈎子函數中使用了 watchEffect,則在元件解除安裝時)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})      

首先,異步操作必須是能中止的異步操作,對于定時器來講中止定時器很容易,clearInterval之類的就可以,但對于ajax來講,需要借助ajax庫(比如axios)提供的中止ajax辦法來中止ajax。

現在我寫一個能直接運作的範例示範一下中止異步操作:

先搭建一個最簡Node伺服器,3000端口的:

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(3000, () => {
  console.log('Server is running...');
});

server.on('request', (req, res) => {
  setTimeout(() => {
    if (/\d.json/.test(req.url)) {
      const data = {
        content: '我是傳回的内容,來自' + req.url
      }
      res.end(JSON.stringify(data));
    }
  }, Math.random() * 3000);
});      
<template>
  <div>
    <div>content: {{ content }}</div>
    <button @click="changePageNumber">第{{ pageNumber }}頁</button>
  </div>
</template>

<script>
import axios from 'axios';
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    let pageNumber = ref(1);
    let content = ref('');

    const changePageNumber = () => {
      pageNumber.value++;
    }

    watchEffect((onInvalidate) => {
      // const CancelToken = axios.CancelToken;
      // const source = CancelToken.source();
      // onInvalidate(() => {
      //   source.cancel();
      // });
      axios.get(`http://localhost:3000/${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,
      changePageNumber,
    };
  },
};
</script>      

上面注釋掉的代碼先保持注釋,然後經過多次瘋狂點選之後,得到這個結果,顯然,内容錯亂了:

Vue3中 watch、watchEffect 詳解

現在取消注釋,重新多次瘋狂點選,得到的結果就正确了:

Vue3中 watch、watchEffect 詳解

除了最後一個請求,上面那些請求有2種結局:

  • 一種是響應的太快,來不及取消的請求,這種請求會傳回200,不過既然它響應太快,沒有任何一次後續 ajax 能夠來得及取消它,說明任何一次後續請求開始之前,它就已經結束了,那麼它一定會被後續某些請求所覆寫,是以這類請求的 content 會顯示一瞬間,然後被後續的請求覆寫,絕對不會比後面的請求還晚。
  • 另一種就是紅色的那些被取消的請求,因為響應的慢,是以被取消掉了。

是以最終結果一定是正确的,而且節省了很多帶寬,也節省了系統開銷。

副作用重新整理時機

Vue 的響應性系統會緩存副作用函數,并異步地重新整理它們,這樣可以避免同一個“tick”中多個狀态改變導緻的不必要的重複調用。

同一個“tick”的意思是,Vue的内部機制會以最科學的計算規則将視圖重新整理請求合并成一個一個的"tick",每個“tick”重新整理一次視圖,如:a=1; b=2; 隻會觸發一次視圖重新整理。$nextTick的Tick就是指這個。

如 watchEffect 監聽了2個變量 count 和 count2,當我調用countAdd,你覺得監聽器會調用2次?

當然不會,Vue會合并成1次去執行。

代碼如下,console.log隻會執行一次:

<template>
  <div>
    <div>{{count}} {{count2}}</div>
    <button @click="countAdd">增加</button>
  </div>
</template>

<script>
import { ref,watchEffect } from 'vue';

export default {
  setup(){
    let count = ref(0);
    let count2 = ref(10);
    const countAdd = () => {
      count.value++;
      count2.value++;
    }
    watchEffect(() => {
      console.log(count.value,count2.value)
    })
    return{
      count,
      count2,
      countAdd
    }
  }
}
</script>      
在核心的具體實作中,元件的 update 函數也是一個被偵聽的副作用。當一個使用者定義的副作用函數進入隊列時,預設情況下,會在所有的元件update前執行。

所謂元件的 update 函數是 Vue 内置的用來更新DOM的函數,它也是副作用,上文已經提到過。

這時候有一個問題,就是預設下,Vue會先執行元件DOM update,還是先執行監聽器?

<template>
  <div>
    <div id="value">{{count}}</div> 
    <button @click="countAdd">增加</button>
  </div>
</template>

<script>
import { ref,watchEffect } from 'vue';

export default {
  setup(){
    let count = ref(0);
    const countAdd = () => {
      count.value++;
    }
    watchEffect(() => {
      console.log(count.value)
      console.log(document.querySelector('#value') && document.querySelector('#value').innerText)
    })
    return{
      count,
      countAdd
    }
  }
}
</script>      

點選若幹次(比如2次)按鈕,得到的結果是:

Vue3中 watch、watchEffect 詳解

為什麼點之前按鈕的innerText列印null?

因為事實就是預設先執行監聽器,然後更新DOM,此時DOM還未生成,當然是null。

當第1和2次點選完,會發現:document.querySelector(‘#value’).innerText 擷取到的總是點選之前DOM的内容。

這也說明,預設Vue先執行監聽器,是以取到了上一次的内容,然後執行元件 update。

Vue 2其實也是這種機制,Vue 2使用 this.$ nextTick() 去擷取元件更新完成之後的 DOM,在 watchEffect 裡就不需要用this.$nextTick()(也沒法用),有一個辦法能擷取元件更新完成之後的DOM,就是使用:

// 在元件更新後觸發,這樣你就可以通路更新的 DOM。
// 注意:這也将推遲副作用的初始運作,直到元件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)      

現在設上 flush 配置項,重新進入元件,再看看:

Vue3中 watch、watchEffect 詳解

是以結論是,如果要操作“更新之後的DOM”,就要配置 flush: ‘post’。

如果要操作“更新之後的DOM ”,就要配置 flush: 'post'。
flush 取值:
  pre (預設)
  post (在元件更新後觸發,這樣你就可以通路更新的 DOM。這也将推遲副作用的初始運作,直到元件的首次渲染完成。)
  sync (與watch一樣使其為每個更改都強制觸發偵聽器,然而,這是低效的,應該很少需要)      

偵聽器調試

onTrack 和 onTrigger 選項可用于調試偵聽器的行為。

  • onTrack 将在響應式 property 或 ref 作為依賴項被追蹤時被調用。
  • onTrigger 将在依賴項變更導緻副作用被觸發時被調用。

這兩個回調都将接收到一個包含有關所依賴項資訊的調試器事件。

建議在以下回調中編寫 debugger 語句來檢查依賴關系:

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)      

onTrack 和 onTrigger 隻能在開發模式下工作。

3. 總結

watch 特點

watch 監聽函數可以添加配置項,也可以配置為空,配置項為空的情況下,watch的特點為:

  • 有惰性:運作的時候,不會立即執行;
  • 更加具體:需要添加監聽的屬性;
  • 可通路屬性之前的值:回調函數内會傳回最新值和修改之前的值;
  • 可配置:配置項可補充 watch 特點上的不足:

    immediate:配置 watch 屬性是否立即執行,值為 true 時,一旦運作就會立即執行,值為 false 時,保持惰性。

    deep:配置 watch 是否深度監聽,值為 true 時,可以監聽對象所有屬性,值為 false 時保持更加具體特性,必須指定到具體的屬性上。

  • 非惰性:一旦運作就會立即執行;
  • 更加抽象:使用時不需要具體指定監聽的誰,回調函數内直接使用就可以;
  • 不可通路之前的值:隻能通路目前最新的值,通路不到修改之前的值;
  • Vue 3 watch 與 Vue 2 的執行個體方法 vm.$ watch(也就是 this.$ watch )的基本用法差不多,隻不過程式員大多使用 watch 配置項,可能對 $watch 執行個體方法不太熟。執行個體方法的一個優勢是更靈活,第一個參數可以接受一個函數,等于是接受了一個 getter 函數。
<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增加了同時監聽多個變量的能力,用數組表達要監聽的變量。回調參數是這種結構:[newR, newS, newT], [oldR, oldS, oldT],不要了解成其他錯誤的結構。
<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>      
  • 被監聽的變量必須是:A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.也就是說,可以是getter/effect函數、ref、Proxy以及它們的數組。絕對不可以是純對象或基本資料。
  • Vue 3的深度監聽還有沒有?當然有,而且預設就是,無需聲明。當然,前提是深層 property 也是響應式的。如果深層 property 無響應式,那麼即便寫上 { deep: true } 也沒用。

繼續閱讀