先写一个简易的聊天发送框:
<template>
<div ref="box" class="wraps">
<div class="item" v-for="item in chatList">
<div>{{ item.name }}:div>
<div>{{ item.message }}div>
div>
div>
<div class="ipt">
<textarea v-model="ipt" type="text" />
<button @click="send">sendbutton>
div>
template>
<script setup lang='ts'>
import { reactive, ref, nextTick, getCurrentInstance, watch } from 'vue'
let chatList = reactive([
{ name: '张三', message: "xxxxxxxxx" },
])
let ipt = ref('')
const send = () => {
chatList.push({ name: '李四', message: ipt.value })
// 为了演示,先不清除ipt框内数据
// ipt.value = ''
}
script>
<style scoped lang='less'>
.wraps {
margin: 10px auto;
width: 500px;
height: 400px;
overflow: auto;
overflow-x: hidden;
background: #fff;
border: 1px solid #ccc;
.item {
width: 100%;
height: 50px;
background: #ccc;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #fff;
}
}
.ipt {
margin: 10px auto;
width: 500px;
height: 40px;
background: #fff;
border: 1px solid #ccc;
textarea {
// 使完全贴合 .ipt
width: 100%;
height: 100%;
border: none;
outline: none;
}
button {
width: 100px;
margin: 10px 0;
float: right;
}
}
style>
但是信息总是保持处于最上面,不能移动到最后最新发送的信息处。
按照我们的逻辑,只需要让最后的可视区到内容顶部的距离为无限大即可看到最新信息。
<template>
<div ref="box" class="wraps">
<div class="item" v-for="item in chatList">
<div>{{ item.name }}:div>
<div>{{ item.message }}div>
div>
div>
<div class="ipt">
<textarea v-model="ipt" type="text" />
<button @click="send">sendbutton>
div>
template>
<script setup lang='ts'>
import { reactive, ref, nextTick, getCurrentInstance, watch } from 'vue'
const chatList = reactive([
{ name: '张三', message: "xxxxxxxxx" },
])
const ipt = ref('')
// 用 ref 获取 box 元素
const box = ref<HTMLDivElement>()
const send = () => {
chatList.push({ name: '李四', message: ipt.value })
// 为了演示,先不清除ipt框内数据
// ipt.value = ''
// scrollTop 元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量
box.value!.scrollTop = 99999
}
script>
<style scoped lang='less'>
.wraps {
margin: 10px auto;
width: 500px;
height: 400px;
overflow: auto;
overflow-x: hidden;
background: #fff;
border: 1px solid #ccc;
.item {
width: 100%;
height: 50px;
background: #ccc;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #fff;
}
}
.ipt {
margin: 10px auto;
width: 500px;
height: 40px;
background: #fff;
border: 1px solid #ccc;
textarea {
// 使完全贴合 .ipt
width: 100%;
height: 100%;
border: none;
outline: none;
}
button {
width: 100px;
margin: 10px 0;
float: right;
}
}
style>
但是发现,此时只是到达最新发出的信息的前一条信息。
原因是:Vue 更新 DOM 是异步的,而数据更新是同步的。
但上述执行的是同步代码,因此出现问题。
所以就需要我们使用 nextTick
。
<template>
<div ref="box" class="wraps">
<div class="item" v-for="item in chatList">
<div>{{ item.name }}:div>
<div>{{ item.message }}div>
div>
div>
<div class="ipt">
<textarea v-model="ipt" type="text" />
<button @click="send">sendbutton>
div>
template>
<script setup lang='ts'>
import { reactive, ref, nextTick, getCurrentInstance, watch } from 'vue'
const chatList = reactive([
{ name: '张三', message: "xxxxxxxxx" },
])
const ipt = ref('')
// 用 ref 获取 box 元素
const box = ref<HTMLDivElement>()
const send = async () => {
chatList.push({ name: '李四', message: ipt.value })
// 为了演示,先不清除ipt框内数据
// ipt.value = ''
// scrollTop 元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量
// 写法一:回调函数模式
// nextTick(() => {
// box.value!.scrollTop = 99999
// })
// 写法二:async 和 await await 下面的代码都是异步的
await nextTick()
box.value!.scrollTop = 99999
}
script>
<style scoped lang='less'>
.wraps {
margin: 10px auto;
width: 500px;
height: 400px;
overflow: auto;
overflow-x: hidden;
background: #fff;
border: 1px solid #ccc;
.item {
width: 100%;
height: 50px;
background: #ccc;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #fff;
}
}
.ipt {
margin: 10px auto;
width: 500px;
height: 40px;
background: #fff;
border: 1px solid #ccc;
textarea {
// 使完全贴合 .ipt
width: 100%;
height: 100%;
border: none;
outline: none;
}
button {
width: 100px;
margin: 10px 0;
float: right;
}
}
style>
成功解决!
总结:操作 DOM 的时候,如果发现数据读取的是上次的,就需要使用 nextTick 。
那么,为什么 Vue 更新 DOM 是要做成异步的的呢?这里我先举一个小栗子:
const current = ref(0)
watch(current, (newVal, oldVal) => {
console.log(newVal, oldVal);
})
for (let i = 0; i < 10000; i++) {
current.value = i
}
结果只输出了最终结果这一次,节省了性能,但后果就是我们操作 DOM 的时候需要把它放到微任务里面,此时的解决方法就是使用 nextTick
。