$nextTick的作用以及与setTimeout的区别,

nextTick() Api介绍

等待下一次 DOM 更新刷新的工具方法。

  • 类型
    function nextTick(callback?: () => void): Promise<void>
    
  • 详细信息
    当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
    nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。
  • 示例
    <script setup>
    import { ref, nextTick } from 'vue'
    
    const count = ref(0)
    
    async function increment() {
      count.value++
    
      // DOM 还未更新
      console.log(document.getElementById('counter').textContent) // 0
    
      await nextTick()
      // DOM 此时已经更新
      console.log(document.getElementById('counter').textContent) // 1
    }
    </script>
    
    <template>
      <button id="counter" @click="increment">{{ count }}</button>
    </template>
    

$nextTick()

绑定在实例上的 nextTick() 函数。

  • 类型
    interface ComponentPublicInstance {
      $nextTick(callback?: (this: ComponentPublicInstance) => void): Promise<void>
    }
    
  • 详细信息
    和全局版本的 nextTick() 的唯一区别就是组件传递给 this.$nextTick() 的回调函数会带上 this 上下文,其绑定了当前组件实例。
    nextTick()
    this.$nextTick()
    

异步更新队列

Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。

$nextTick()适用场景

  1. 改变数据后更新DOM元素
    <template>
      <div class="message" ref="messageRef">
        {{ message }}
      </div>
      <button @click="updateMessage">更新Message</button>
    </template>
    <script lang="ts" setup>
    import { nextTick, onMounted, ref } from 'vue';
    const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可
    let message = ref('Message')
    onMounted(() => {
      if (messageRef.value) {
        console.log(messageRef.value)
      }
    })
     const updateMessage = () => {
     	message.value = 'Updated Message'
     	// 如果没有用nextTick,页面上展示 Updated Message, 如果用了nextTick 页面展示DOM Message
        nextTick(() => {
          messageRef.value.textContent = 'DOM Message';
        })
     }
    </script>
    
  2. 获取更新后的DOM尺寸和位置
    	<template>
      <div class="message" ref="messageRef">
        {{ message }}
      </div>
      <button @click="getSize">获取Div尺寸</button>
    </template>
    
    <script lang="ts" setup>
    import { nextTick, onMounted, ref } from 'vue';
    
    const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可
    let message = ref('Message')
    onMounted(() => {
      if (messageRef.value) {
        console.log(messageRef.value)
      }
    })
     const getSize = () => {
      message.value = 'Updated Message'
      nextTick(() => {
        console.log(messageRef.value?.offsetWidth, messageRef.value?.offsetHeight)
      })
     }
    </script>
    
    在这里插入图片描述
  3. 执行复杂的计算
    	<template>
      <div class="message" ref="messageRef">
        {{computedValue}}
      </div>
    </template>
    
    <script lang="ts" setup>
    import { nextTick, onMounted, ref, computed } from 'vue';
    
    const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可
    let message = ref('Message')
    let items = ref([1, 2, 3, 4, 5]);
      onMounted(() => {
        if (messageRef.value) {
          console.log(messageRef.value) //  <div class="message" ref="messageRef">15</div>
        }
      })
      const computedValue = computed(() => {
        // 执行复杂的计算
        const total = items.value.reduce((sum, val) => sum + val, 0)
        console.log(`Total1: ${total}`) // Total1: 15
        console.log(document.getElementsByClassName('message').length) // 0
        // 确保下一个 DOM 周期中更新视图
        nextTick(() => {
          console.log(`Total2: ${total}`) // Total1: 15
          console.log(document.getElementsByClassName('message')[0].textContent) // 15
        })
        return total
    
      })
    
  4. 在父组件中,等待子组件数据更新后再执行操作
    //child.vue
    <template>
      <div>{{message}}</div>
    </template>
    
    <script lang="ts" setup>
      import { defineExpose, ref } from 'vue';
      let message = ref('Message');
      function updateMessage() {
        message.value = 'Updated Message'
      }
      // 将父组件要调用的方法抛出去
      defineExpose({updateMessage})
    </script>
    // Parent.vue
    <template>
      <Child ref="childRef" />
    </template>
    
    <script lang="ts" setup>
      import { nextTick, onMounted, ref, computed } from 'vue';
      import Child from "@/views/child.vue";
    
      let childRef = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可
      onMounted(() => {
        console.log(childRef.value);
        nextTick(() => {
          console.log(childRef.value);
          childRef.value.updateMessage()
        })
      })
    </script>
    
  5. 等待 Vue.js 插件初始化后再执行操作
    <template>
      <input v-model="message" ref="childRef" />
    </template>
    
    <script lang="ts" setup>
      import { nextTick, onMounted, ref } from 'vue';
    
      let childRef = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可
      let message = ref('');
      onMounted(() => {
        nextTick(() => {
          childRef.value?.focus();
        })
      })
    </script>
    
  6. 监听视图变化并执行相应操作
    <template>
      <div>
        <input ref="inputMessage" v-model="message">
        <div>Message Length: {{messageLength}}</div>
      </div>
    </template>
    
    <script lang="ts" setup>
      import { computed, nextTick, onMounted, ref } from 'vue';
    
      let inputMessage = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可
      let message = ref('');
      let messageLength = computed(() => {
        return message.value.length
      })
      onMounted(() => {
        focusInput();
      })
      function focusInput() {
        nextTick(() => {
          inputMessage.value?.focus();
        })
      }
    </script>
    

在这里插入图片描述

$nextTick和setTimeout区别

  1. nextTick 在vue 源码中是利用 Promise.resolve()实现的。该问题实际就是Promise与setTimeout的区别,本质是Event Loop中微任务与宏任务的区别。
    nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
  2. setTimeout:只是延迟执行,在延迟执行的方法里,DOM有可能会更新也有可能没有更新。
  3. setTimeout:就是个延时回调,和DOM操作无关。
  4. 建议使用nextTick在有涉及DOM更新的场景

JS中的Event Loop

javascript是单线程的,所有的任务都会在主线程中执行的,当主线程中的任务都执行完成之后,系统会 “依次” 读取任务队列里面的事件,因此对应的异步任务进入主线程,开始执行。
但是异步任务队列又分为: macrotasks(宏任务) 和 microtasks(微任务)。 他们两者分别有如下API:

macrotasks(宏任务): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
microtasks(微任务): Promise、process.nextTick、MutationObserver 等。
promise的then方法的函数会被推入到 microtasks(微任务) 队列中(Promise本身代码是同步执行的),而setTimeout函数会被推入到 macrotasks(宏任务) 任务队列中,在每一次事件循环中 macrotasks(宏任务) 只会提取一个执行,而 microtasks(微任务) 会一直提取,直到 microtasks(微任务)队列为空为止。

也就是说,如果某个 microtasks(微任务) 被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个 macrotasks(宏任务), 主线程执行完成该任务后又会循环检查 microtasks(微任务) 队列是否还有未执行的,直到所有的执行完成后,再执行 macrotasks(宏任务)。 依次循环,直到所有的异步任务完成为止。

console.log(1);
 setTimeout(function(){
   console.log(2);
  }, 0);
new Promise(function(resolve) {
   console.log(3);
   for (var i = 0; i < 100; i++) {
     i === 99 && resolve(i);
   }
   console.log(4);
 }).then(function() {
   console.log(5);
 });
 console.log(6);

上述代码打印结果

console.log(1);
setTimeout(function(){
  console.log(2);
}, 10);
new Promise(function(resolve) {
  console.log(3);
  for (var i = 0; i < 10000; i++) {
    i === 9999 && resolve(i);
  }
  console.log(4);
}).then(function() {
  console.log(5);
});
setTimeout(function(){
  console.log(7);
},1);
new Promise(function(resolve) {
  console.log(8);
  resolve();
}).then(function(){
  console.log(9);
});
console.log(6); 

上述代码打印结果

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值