Vue3中 watchwatchEffect 详解

Posted 明天也要努力

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue3中 watchwatchEffect 详解相关的知识,希望对你有一定的参考价值。

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>


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>


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>


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>


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>

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>


停止侦听
当 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>


清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (场景:有一个页码组件里面有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>

上面注释掉的代码先保持注释,然后经过多次疯狂点击之后,得到这个结果,显然,内容错乱了:


现在取消注释,重新多次疯狂点击,得到的结果就正确了:


除了最后一个请求,上面那些请求有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次)按钮,得到的结果是:


为什么点之前按钮的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,就是使用:

// 在组件更新后触发,这样你就可以访问更新

以上是关于Vue3中 watchwatchEffect 详解的主要内容,如果未能解决你的问题,请参考以下文章

Vue3中 响应式 API ( readonlyshallowReadonlytoRawmarkRaw ) 详解

Vue3中 响应式 API ( readonlyshallowReadonlytoRawmarkRaw ) 详解

Vue3中 响应式 API ( readonlyshallowReadonlytoRawmarkRaw ) 详解

Vue3中 响应式 API( shallowReactiveshallowReftriggerRef customRef )详解

vue3.0 diff算法详解(超详细)

Vue3中 内置组件 Teleport 详解