nexttick是怎么可以获取到更新后的dom的_记一个nextTick引发的bug

本文探讨了在Vue开发中nextTick的使用,详细解释了为何nextTick能够在DOM更新后执行回调。通过一个示例,揭示了由于v-if导致的无法获取更新后DOM的bug,并提出了三种解决方法:确保触发DOM更新操作、使用setTimeout或改用v-show。同时,文章引用了Vue官方文档和源码解析来辅助理解。
摘要由CSDN通过智能技术生成

在使用Vue开发的过程中,如果我们想在更新DOM结束之后执行回调,我们可以使用Vue的nextTick方法。然而在一次日常开发中,我却遇到了nextTick执行时无法获取到更新后的DOM的问题。下面是排查过程中的一些记录:

一个例子

下面是能够复现这个bug的简单demo:

<template>
  <div v-if="isLoading" id="loading">加载中</div>
  <div v-else id="app">
    <ul>
      <li v-for="item in list" :key="item.id" :id="'floor-' + item.id">floor: {{item.id}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isLoading: true,
      list: [],
      id: 0
    };
  },
  methods: {
    delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    },
    genList(count) {
      const result = [];
      for (let i = 0; i < count; i++) {
        result.push({ id: this.id++ });
      }
      return result;
    },
    getApp() {
      return document.getElementById("app");
    },
  },
  async mounted() {
    // 模拟异步过程
    await this.delay(200);

    // 修改data
    this.list = this.genList(100);

    // 在nextTick中尝试获取更新后的列表元素
    this.$nextTick(() => {
      const floor = document.getElementById("floor-50");
      console.log("nextTick1 before loading floor: ", floor);
    });

    this.isLoading = false;
  }
};
</script>

this.list 是要渲染的列表,按理说修改了this.list的值后马上调用nextTick,应该能在回调函数里获取到id为floor-50的dom元素。然而打印的结果却是null,表明在回调函数执行时floor-50这个dom元素还没渲染出来,为什么会这样呢?

nextTick的文档

回头再看nextTick的文档说明: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

在上面的例子中,我们确实是在修改数据后立即调用了nextTick,但是所谓的下次DOM更新循环又是指什么时候呢?翻阅文档后没有找到对应的说明,我们只好去看看Vue源码里nextTick的实现方式了。

nextTick的作用

nextTick的源码实现在这里:vue/next-tick.js at dev · vuejs/vue · GitHub,总共只有100行左右,大部分都是兼容性代码。 简单来说,在主线程的执行过程中(一个tick内)注册的nextTick回调,会在tick结束后执行。那为什么在修改数据后立即使用nextTick就能在回调中获取到更新后的dom呢?原因就是Vue的更新dom的时候,也是在nextTick中执行的。

响应式更新的时机

对Vue响应式更新原理有所了解的同学都知道,在template中使用到的data会作为组件的依赖被收集起来,所以当我们对组件中响应的数据做了修改,会触发setter的逻辑,从而通知这个依赖的所有Watcher触发一次update。这个update走到最后会调用一个nextTick(flushSchedulerQueue),这里就是为什么我们能在nextTick访问到更新后的dom的关键:

当我们修改了data之后,nextTick callback:

this.list = this.genList(100); 
// 理论上此时的nextTick callback队列: [flushSchedulerQueue]

然后我们通过nextTick注册回调,打印dom:

this.list = this.genList(100);
// 理论上此时的nextTick callback队列: [flushSchedulerQueue]

this.$nextTick(() => {
  const floor = document.getElementById("floor-50");
  console.log("nextTick1 before loading floor: ", floor);
});
// 理论上此时的nextTick callback队列: [flushSchedulerQueue, console]

可以看到,我们的console是在dom更新操作后执行的,因此应该能访问到更新后的dom。

出现bug的原因

按照上面的分析,似乎问题就出在this.list = this.genList(100)这行代码执行后,Vue没有把flushSchedulerQueue放进nextTick的队列里。再仔细看看例子里template的代码,我们发现了问题所在:

<template>
  <div v-if="isLoading" id="loading">加载中</div>
  <div v-else id="app">
    <ul>
      <li v-for="item in list" :key="item.id" :id="'floor-' + item.id">floor: {{item.id}}</li>
    </ul>
  </div>
</template>

由于使用了v-if,所以首次渲染时,该组件的依赖其实只有isLoadinglist由于在v-else中,修改它并不会触发视图更新,所以例子中的代码执行后的nextTick callback队列如下:

this.list = this.genList(100);
// 此时的nextTick callback队列: []

this.$nextTick(() => {
  const floor = document.getElementById("floor-50");
  console.log("nextTick1 before loading floor: ", floor);
});
// 此时的nextTick callback队列: [console]

this.isLoading = false
// 此时的nextTick callback队列: [console, flushSchedulerQueue]

可以看到,console在flushSchedulerQueue之前执行,所以自然就获取不到更新后的dom元素了。

根据这个原理,如果我们想修复这个bug,其实有三种方式:

  1. 确保触发一个会更新dom的操作,如上述例子中提前执行this.isLoading=false,再调用nextTick
  2. 改为setTimeout, 走macroTask。macroTask一定在microTask之后执行,缺点就是setTimeout会带来一定的延时。
  3. v-if改v-show,从而使修改的data能触发视图更新

完整的demo在这里,感兴趣的小伙伴可以自己动手尝试一下:

https://codesandbox.io/s/nexttick-work-unexpected-td7t5​codesandbox.io

参考资料

[1] https://cn.vuejs.org/v2/api/#Vue-nextTick

[2] vue/next-tick.js at dev · vuejs/vue · GitHub

[3] 派发更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值