Vue3中 watch、watchEffect 详解

简介: 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>

2020062310470442.png

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>

2020062310470442.png

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>

2020062310470442.png

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>

2020062310470442.png

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>

2020062310470442.png

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>

2020062310470442.png

停止侦听

当 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>

2020062310470442.png

清除副作用

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

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

2020062310470442.png

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

2020062310470442.png

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

2020062310470442.png

为什么点之前按钮的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 配置项,重新进入组件,再看看:

2020062310470442.png

所以结论是,如果要操作“更新之后的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 时保持更加具体特性,必须指定到具体的属性上。

watchEffect 特点

  • 非惰性:一旦运行就会立即执行;
  • 更加抽象:使用时不需要具体指定监听的谁,回调函数内直接使用就可以;
  • 不可访问之前的值:只能访问当前最新的值,访问不到修改之前的值;

Vue 3 watch 与 Vue 2 watch 对比

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 } 也没用。


相关文章
|
29天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
129 64
|
4天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
22 3
|
29天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
33 8
|
28天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
31 1
|
28天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
39 1
|
8天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
43 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
30 1
|
1月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
38 1
vue学习第四章
|
1月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
26 1
vue学习第7章(循环)

热门文章

最新文章