[VUE]带你深入思考watch

1,425 阅读5分钟

前言

与其他渲染框架相比,双向绑定一直都是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 += 1this.num += 2this.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的发展变化,以及提出了业务例子的解决方法。我个人一直都认为从实际业务问题,来对一个工具或者框架作出思考是一个很好的学习方式。因为在这个过程中,我们不但可以一步一步地解决我们的问题。同时又可以看到别人在处理这些问题的时候的方案理念,从中学习。希望看到这里的你,也可以有所所收获,一起努力。

如果觉得本文对你有一点帮助的话,可以给我点下赞噢~