前言
与其他渲染框架相比,双向绑定一直都是VUE的独门优势。而watch API作为一个提供给用户自行触发响应回调的方法,更是为用户提供解决业务提供了有力的武器。今天我们就来从VUE2到VUE3中watch的发展中看一下watch的改变,以及从中学习对应的知识。
我们熟知的watch
在VUE 2 中watch作为一个组件的对象属性存在,我们可以在watch中以想要监听的data,prop或者store值为key,然后定义值改变后的回调函数。如下例子:
export default {
name: 'APP',
data(){
return {
num:1
}
},
watch:{
num(newValue,oldValue){
console.log('num改变了!')
}
}
}
触发时机
但在实际开发中使用watch API我们还会遇到一些问题。
首次数据初始化,并不会触发watch
对于这个问题,VUE给我们提供了immediate配置,通过配置我们可以强制VUE在初次渲染时,触发一下watch的回调。
watch:{
num: {
handler:(newValue,oldValue){
console.log(oldValue) // 首次触发时oldValue会是undefined
console.log('num改变了!')
}
immediate: true
},
}
多次修改监听值,watch只会触发一次
对于上方的问题官方给出了标准答案,但并不是所有问题都能找到官方解答。如下列例子:
data(){
return {
num:1,
count:0,
}
},
watch:{
num(newValue,oldValue){
this.count++;
console.log(this.count);
}
},
created(){
this.num += 1;
this.num += 2;
this.num += 3;
}
我希望可以通过监听num并利用count来记录num被修改了多少次,例子中我在created生命周期中连续对num进行了3次修改。此时我希望控制台输出的结果应该是3。可是实际上输出的却是1。
num(newValue,oldValue){
this.count++;
console.log(this.count); // 1
}
这意味着watch的函数只被触发了1次,那么这是为什么呢?
watch的“优化”
我们都知道VUE的最大卖点就是双向绑定,定义watch最大的作用就是帮我们“监听”一个data,在data改变时触发一个回调。同时我们也知道在VUE中DOM的更新渲染是异步的,在同一个事件循环中VUE会在最后才用data数据刷新DOM。这样的好处就是可以大大降低DOM的渲染成本。
这么看来,我们就可以猜想watch也许也是用了同样的机制。而通过源码可以发现,watch的触发跟data监听的数据一样,作为与Dep关联的一个watcher存在。每次watcher的update被触发都会执行一个叫的queueWatcher 函数,下面是这个函数的源码:
function queueWatcher (watcher) {
var id = watcher.id;
// 根据watcher的id判断,每个watcher只会被执行一次
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue();
return
}
// watch的真正调用在这里,利用nextTick调用
nextTick(flushSchedulerQueue);
}
}
}
因此我们知道了nextTick优化在某程度上是对watch的调用作出了限制的。
VUE 3中的watch
虽然VUE 3的数据监听底层机制改用了proxy并作了很多的新优化,但与VUE2 相同的是,VUE 3的watch本质上也是基于数据响应的机制实现的。所以在默认场景下watch的触发也会有上面我们讨论的问题,但在VUE3中官方给出了解决办法。VUE3中给watch新增了一些配置:
根据文档,我们只要把配置flush设置为sync,就能实现我们想要的结果了。
// VUE3 setup
const myData = ref(2);
watch(
myData,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
flush: "sync",
}
);
const main = () => {
myData.value++;
myData.value++;
myData.value++;
myData.value++;
};
// 输出
// 3 2
// 4 3
// 5 4
// 6 5
遗弃了的async配置
相信眼力好的朋友在刚刚的queueWatcher源码已经也找到了一个async配置,从代码上看它似乎也可以实现VUE3 flush的效果。
但是很遗憾这个配置已经在VUE2的官方文档中被移除了,官方认为这会影响渲染的性能。
遗弃的最大原因是这是一个全局的配置,一旦配置了整个项目的所以watcher的触发都会被影响,因此必然会影响渲染成本。但VUE3中watch的flush配置则是一个可以给每个监听单独配置的项,大大提高了业务的自定义度。
合理利用nextTick
其实在VUE3的官方文档中也提到了,尽管官方提供了flush的配置,但他们并不希望用户经常使用。在VUE的框架体系中,nextTick其实会是一个更好的选择。
nextTIck是VUE事件循环中一个很关键的函数,无论是用户自己定义的还是框架生命周期的默认回调最后都会在nextTick中调用。VUE在向用户暴露nextTick的同时,其实也就意味着向用户暴露很大的操作VUE事件循环的自由度。在上述的例子中,我们其实也能通过nextTick实现我们想要的结果。
// VUE 2实现
created(){
this.setA();
},
methods:{
async setA() {
this.a += 1;
await this.$nextTick();
this.a += 2;
await this.$nextTick();
this.a += 3;
},
}
// VUE 3实现
import { nextTick,ref } from 'vue'
const myData = ref(2);
watch(
myData,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
const main = async() => {
myData.value++;
await nextTick();
myData.value++;
await nextTick();
myData.value++;
};
从框架角度上来看,其实nextTick会是更好的选择,因为用户可以自己在业务中决定是否需要使用同步方式触发回调。同时又不会破坏框架的运行机制,保持事件循环机制的稳定,避免出现意想不到的问题。
总结
今天我们从一个业务例子出发,一起了解了VUE的watch的实现逻辑。简单回顾了一下watch从VUE2到3的发展变化,以及提出了业务例子的解决方法。我个人一直都认为从实际业务问题,来对一个工具或者框架作出思考是一个很好的学习方式。因为在这个过程中,我们不但可以一步一步地解决我们的问题。同时又可以看到别人在处理这些问题的时候的方案理念,从中学习。希望看到这里的你,也可以有所所收获,一起努力。
如果觉得本文对你有一点帮助的话,可以给我点下赞噢~