Vue3-nextTick不靠谱

2,253 阅读3分钟

前言

  • nextTick()官方文档的定义:等待下一次 DOM 更新刷新的工具方法
  • nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。

但是在 nextTick() 回调函数或者 await 返回的 Promise 后获取DOM能确保是最新的吗?下面请看例子:

例子1

通过defineAsyncComponent引入子组件

<Child />子组件:

<template>
  <div>
    Child
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  console.log('mounted Child')
})
</script>

父组件:

<template>
  <button @click="showChild">显示子组件</button>
  <div id="container" v-if="show">
    <Child />
  </div>
</template>

<script setup>
import { defineAsyncComponent, ref, nextTick } from "vue";
const Child = defineAsyncComponent(() => import("./Child.vue"));

const show = ref(false);

function nextTickCallback() {
  console.log('nextTickCallback')
  console.log("container内容:" + document.getElementById("container").textContent);
}
function showChild() {
  show.value = true;
  nextTick(nextTickCallback);
}
</script>

效果: defineAsyncComponent.gif 根据运行效果,我们发现nextTickCallbackmounted Child 之前,并且没有正确获取到DOM的状态。那么是因为container的内容是Vue组件的原因吗?为了解答疑问,我们再看一个例子:

例子2

直接import子组件

<template>
  <button @click="showChild">显示子组件</button>
  <div id="container" v-if="show">
    <Child />
  </div>
</template>

<script setup>
import { ref, nextTick } from "vue";
import Child from './Child.vue'

const show = ref(false);

function nextTickCallback() {
  console.log('nextTickCallback')
  console.log("container内容:" + document.getElementById("container").textContent);
}
function showChild() {
  show.value = true;
  nextTick(nextTickCallback);
}
</script>

importComp.gif 上面例子把defineAsyncComponent惰性加载组件改为直接import,我们发现nextTickCallbackmounted Child 之后,并且能够正确获取DOM状态了,所以把defineAsyncComponent改为直接导入,就一定能获取到正确的DOM状态吗?我们再再看一个例子:

例子3

子组件async setup()

<Child />子组件:

<template>
  <div>
    Child
  </div>
</template>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log('mounted Child')
})

await Promise.resolve()

</script>

父组件:

<template>
  <button @click="showChild">显示子组件</button>
  <div id="container" v-if="show">
    <Suspense>
      <Child />
    </Suspense>
  </div>
</template>

<script setup>
import { ref, nextTick } from "vue";
import Child from './Child.vue'

const show = ref(false);

function nextTickCallback() {
  console.log('nextTickCallback')
  console.log("container内容:" + document.getElementById("container").textContent);
}
function showChild() {
  show.value = true;
  nextTick(nextTickCallback);
}
</script>

asyncSetup.gif 上面例子把子组件改为async setup()后,我们发现nextTickCallbackmounted Child 之前,也没有正确获取到DOM的状态。

如何保证渲染完成再执行相应操作?

前面的例子举证的是修改状态后,立即获取DOM内容,但是这种场景在实际开发的过程中比较少见。以下才是更加常见的例子:

<Child />子组件:

<template>
  <div>
    Child
  </div>
</template>

<script setup>
import { onMounted, defineExpose } from 'vue'
onMounted(() => {
  console.log('mounted Child')
})

const say = () => {
  console.log('hi')
}

defineExpose({
  say
})
</script>

父组件:

<template>
  <button @click="showChild">显示子组件</button>
  <div v-if="show">
    <Child ref="childRef" />
  </div>
</template>

<script setup>
import { defineAsyncComponent, ref, nextTick } from "vue";
const Child = defineAsyncComponent(() => import("./Child.vue"));

const show = ref(false);

const childRef = ref();

function showChild() {
  show.value = true;
  nextTick(() => {
    // 报错:TypeError: Cannot read properties of undefined (reading 'say')
    childRef.value.say();
  });
}
</script>

runNextTick.gif 以上例子,需要渲染子组件后,立即调用子组件内部的say方法,但是在nextTick回调里面<Child />还没有mounted,所以无法通过ref访问。 要想在子组件渲染完成后,立即执行子组件内部的方法,我们可以通过监听VNode 生命周期事件实现:

<template>
  <button @click="showChild">显示子组件</button>
  <div v-if="show">
    <Child ref="childRef" @vue:mounted="mountedChild" />
  </div>
</template>

<script setup>
import { defineAsyncComponent, ref, nextTick } from "vue";
const Child = defineAsyncComponent(() => import("./Child.vue"));

const show = ref(false);

const childRef = ref();

let mountedChildCallback = new Set()
const mountedChild = () => {
  mountedChildCallback.forEach((cb) => cb());
  mountedChildCallback = null;
};

function showChild() {
  show.value = true;
  // 通过判断mountedChildCallback是否为Truthy判断子组件是否mounted
  if (mountedChildCallback) {
    mountedChildCallback.add(() => {
       childRef.value.say();
    })
  } else {
    childRef.value.say();
  }
}
</script>

vueMounted.gif

总结

  1. DOM内容为async setup()组件和异步组件nextTick的回调函数不能获取到“正确”的DOM内容。
  2. async setup()组件和异步组件可通过监听VNode 生命周期事件实现子组件渲染完后立即调用内部方法。

结语

感谢您耐心阅读这篇文章。如果您觉得内容对您有帮助或启发,请不吝点赞支持。如果您发现文章中的任何错误或需要改进的地方,欢迎您指正批评