阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

响应式基础

使用 ref

可以使用 ref 创建一个响应式的数据:

<template>
  <div>{{ name }}</div>
</template>

<script setup>
import { ref } from 'vue'
// 现在的 name 就是一个响应式数据
let name = ref('Bill')
console.log(name)
console.log(name.value)
setTimeout(() => {
  name.value = 'Tom'
}, 2000)
</script>

<style lang="scss" scoped></style>

ref 返回的响应式数据是一个对象,我们需要通过 .value 访问到内部具体的值。模板中之所以不需要 .value,是因为在模板会对 ref 类型的响应式数据自动解包。

ref 可以持有任意的类型,可以是对象、数组、普通类型的值、Map、Set…

对象的例子:

<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
</template>

<script setup>
import { ref } from 'vue'
// 现在的 name 就是一个响应式数据
let Bill = ref({
  name: 'Biil',
  age: 18
})
setTimeout(() => {
  Bill.value.name = 'Biil2'
  Bill.value.age = 20
}, 2000)
</script>

<style lang="scss" scoped></style>

数组的例子:

<template>
  <div>{{ arr }}</div>
</template>

<script setup>
import { ref } from 'vue'
// 现在的 name 就是一个响应式数据
let arr = ref([1, 2, 3])
setTimeout(() => {
  arr.value.push(4, 5, 6)
}, 2000)
</script>

<style lang="scss" scoped></style>

第二个点,ref 所创建的响应式数据是具备深层响应式,这一点主要体现在值是对象,对象里面又有嵌套的对象:

<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
  <div>{{ Bill.nested.count }}</div>
</template>

<script setup>
import { ref } from 'vue'
// 现在的 name 就是一个响应式数据
let Bill = ref({
  name: 'Biil',
  age: 18,
  nested: {
    count: 1
  }
})
setTimeout(() => {
  Bill.value.name = 'Biil2'
  Bill.value.age = 20
  Bill.value.nested.count += 2
}, 2000)
</script>

<style lang="scss" scoped></style>

如果想要放弃深层次的响应式,可以使用 shallowRef,通过 shallowRef 所创建的响应式,不会深层地递归将对象每一层转为响应式,而只会将 .value 的访问转为响应式:

const state = shallowRef({ count: 1 });
// 这个操作不会触发响应式更新
state.value.count += 2;
// 只针对 .value 值的更改会触发响应式更新
state.value = { count: 2 };

具体示例:

<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
  <div>{{ Bill.nested.count }}</div>
</template>

<script setup>
import { shallowRef } from 'vue'
let Bill = shallowRef({
  name: 'Biil',
  age: 18,
  nested: {
    count: 1
  }
})
// 下面的更新不会触发视图更新
setTimeout(() => {
  Bill.value.name = 'Biil2'
  Bill.value.age = 20
  Bill.value.nested.count += 2
}, 2000)
// 下面的更新会触发视图更新
setTimeout(() => {
  Bill.value = {
    name: 'Biil3',
    age: 30,
    nested: {
      count: 3
    }
  }
}, 4000)
</script>

<style lang="scss" scoped></style>

响应式数据的更新,带来了 DOM 的自动更新,但是这个 DOM 的更新并非是同步的,这意味着当响应式数据发生修改后,我们去获取 DOM 值,拿到的是之前的 DOM 数据:

<template>
  <div id="container">{{ count }}</div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
let count = ref(1)
let container = null
setTimeout(() => {
  count.value = 2 // 修改响应式状态
  console.log('第二次打印:', container.innerText)
}, 2000)
// 这是一个生命周期钩子方法
// 会在组件完成初始渲染并创建 DOM 节点后自动调用
onMounted(() => {
  container = document.getElementById('container')
  console.log('第一次打印:', container.innerText)
})
</script>

<style lang="scss" scoped></style>

如果想要获取最新的 DOM 数据,可以使用 nextTick,这是 Vue 提供的一个工具方法,会等待下一次的 DOM 更新,从而方便后面能够拿到最新的 DOM 数据。

<template>
  <div id="container">{{ count }}</div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'
let count = ref(1)
let container = null
setTimeout(async () => {
  count.value = 2 // 修改响应式状态
  // 等待下一个 DOM 更新周期
  await nextTick()
  // 这个时候再打印就是最新的值了
  console.log('第二次打印:', container.innerText)
}, 2000)
// 这是一个生命周期钩子方法
// 会在组件完成初始渲染并创建 DOM 节点后自动调用
onMounted(() => {
  container = document.getElementById('container')
  console.log('第一次打印:', container.innerText)
})
</script>

<style lang="scss" scoped></style>

如果不用 async await,那么就是通过回调的形式:

setTimeout(() => {
    count.value = 2; // 修改响应式状态
    // 等待下一个 DOM 更新周期
    nextTick(() => {
        // 这个时候再打印就是最新的值了
        console.log("第二次打印:", container.innerText);
    });
}, 2000);

当然还是推荐使用 async await,看上去代码的逻辑更加清晰一些。

使用 reactive

reactive 通常将一个对象转为响应式对象

<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
</template>

<script setup>
import { reactive } from 'vue'
const state = reactive({
  count1: 0,
  nested: {
    count2: 0
  }
})
setTimeout(()=>{
  state.count1++
  state.nested.count2 += 2;
},2000);
</script>

<style lang="scss" scoped></style>

Vue 中的响应式底层是通过 ProxyAPI 来实现的,但是这个 ProxyAPI 只能对对象进行拦截,无法对原始值进行拦截。

这里就会产生一个问题:如果用户想要把一个原始值转为响应式,该怎么办?

两种方案:

  1. 让用户自己处理,用户需要将自己想要转换的原始值包装为对象,然后再使用 reactive API 🙅
  2. 框架层面来处理,多提供一个 API,这个 API 可以帮助用户简化操作,将原始值也能转为响应式数据 🙆

ref 的背后其实也调用了 reactive API

  • 原始值:Object.defineProperty
  • 复杂值:reactive API

reactive 还有一个相关的 API shallowReactiveAPI,是浅层次的,不会深层次去转换成响应式

<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
</template>

<script setup>
import { shallowReactive } from 'vue'
const state = shallowReactive({
  count1: 0,
  nested: {
    count2: 0
  }
})
setTimeout(()=>{
  state.count1++
},2000);
setTimeout(()=>{
  state.nested.count2++
},4000)
</script>

<style lang="scss" scoped></style>

使用细节

先说最佳实践:尽量使用 ref 来作为声明响应式数据的主要 API.

reactive 局限性

  1. 使用 reactvie 创建响应式数据的时候,值的类型是有限的
    • 只能是对象类型(object、array、map、set)
    • 不能够是简单值(string、number、boolean)
  2. 第二条算是一个注意点,不能够去替换响应式对象,否则会丢失响应式的追踪
let state = reactive({ count: 0 });
// 下面的这个操作会让上面的对象引用不再被追踪,从而导致上面对象的响应式丢失
state = reactive({ count: 1 });
  1. 对解构操作不友好,当对一个 reactvie 响应式对象进行解构的时候,也会丢失响应式
let state = reactive({ count: 0 });
// 当进行解构的时候,解构出来的是一个普通的值
let { count } = state;
count++; // 这里也就是单纯的值的改变,不会触发和响应式数据关联的操作

// 另外还有函数传参的时候
// 这里传递过去的也就是一个普通的值,没有响应式
func(state.count);

ref 解包细节

所谓 ref 的解包,指的是自动访问 value,不需要再通过 .value 去获取值。例如模板中使用 ref 类型的数据,就会自动解包。

  • ref 作为 reactvie 对象属性

这种情况下也会自动解包

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

<script setup>
import { ref, reactive } from 'vue'
const name = ref('Bill')
const state = reactive({
  name
})
console.log(state.name) // 这里会自动解包
console.log(name.value)
</script>

<style lang="scss" scoped></style>

如果 ref 作为 shallowReactive 对象的属性,那么不会自动解包

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

<script setup>
import { ref, shallowReactive } from 'vue'
const name = ref('Bill')
const state = shallowReactive({
  name
})
console.log(state.name.value) // 不会自动解包
console.log(name.value)
</script>

<style lang="scss" scoped></style>

因为对象的属性是一个 ref 值,这也是一个响应式数据,因此 ref 的变化会引起响应式对象的更新

<template>
  <div>
    <div>{{ state.name.value }}</div>
  </div>
</template>

<script setup>
import { ref, shallowReactive } from 'vue'
const name = ref('Bill')
const state = shallowReactive({
  name
})
setTimeout(() => {
  name.value = 'Tom'
},2000)
</script>

<style lang="scss" scoped></style>

【课堂练习】下面的代码:

  1. 为什么 Bill 渲染出来有双引号?
  2. 为什么 2 秒后界面没有渲染 Smith ?
<template>
  <div>{{ obj.name }}</div>
</template>

<script setup>
import { ref, shallowReactive } from 'vue'
const name = ref('Bill') // name 是一个 ref 值
const obj = shallowReactive({
  name
})
setTimeout(() => {
  obj.name = 'John'
}, 1000)
setTimeout(() => {
  name.value = 'Smith'
}, 2000)
</script>

<style lang="scss" scoped></style>

答案:

  1. 因为使用的是 shallowReactive,shallowReactive 内部的 ref 是不会自动解包的
  2. 1 秒后,obj.name 被赋予了 John 这个字符串值,这就使得和原来的 ref 数据失去了联系

如果想要渲染出 Smith,修改如下:

import { ref, shallowReactive } from "vue";
const name = ref("Bill"); // name 是一个 ref 值
const obj = shallowReactive({
    name,
});
setTimeout(() => {
    obj.name.value = "John";
}, 1000);
setTimeout(() => {
    name.value = "Smith";
}, 2000);

下面再来看一个例子:

<template>
  <div>{{ obj.name.value }}</div>
</template>

<script setup>
import { ref, shallowReactive } from 'vue'
const name = ref('Bill');
const stuName = ref('John');

const obj = shallowReactive({name})

// 注意这句代码,意味着和原来的 name 这个 Ref 失去关联
obj.name = stuName;

setTimeout(()=>{
  name.value = 'Tom';
}, 2000)

setTimeout(()=>{
  stuName.value = 'Smith';
}, 4000)
</script>

<style lang="scss" scoped></style>
  • 数组和集合里面使用 ref

如果将 ref 数据作为 reactvie 数组或者集合的一个元素,此时是不会自动解包的

// 下面这些是官方所给的例子
const books = reactive([ref("Vue 3 Guide")]);
// 这里需要 .value
console.log(books[0].value);

const map = reactive(new Map([["count", ref(0)]]));
// 这里需要 .value
console.log(map.get("count").value);
<template>
  <div></div>
</template>

<script setup>
import { ref, reactive } from 'vue'
const name = ref('Bill')
const score = ref(100)
const state = reactive({
  name,
  scores: [score]
})
console.log(state.name) // 会自动解包
console.log(state.scores[0]) // 不会自动解包
console.log(state.scores[0].value) // 100
</script>

<style lang="scss" scoped></style>
  • 在模板中的自动解包

在模板里面,只有顶级的 ref 才会自动解包。

<template>
  <div>
    <div>{{ count }}</div>
    <div>{{ object.id }}</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0) // 顶级的 Ref 自动解包
const object = {
  id: ref(1) // 这就是一个非顶级 Ref 不会自动解包
}
</script>

<style lang="scss" scoped></style>

上面的例子,感觉非顶级的 Ref 好像也能够正常渲染出来,仿佛也是自动解包了的。

但是实际情况并非如此。

<template>
  <div>
    <div>{{ count + 1 }}</div>
    <div>{{ object.id + 1 }}</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0) // 顶级的 Ref 自动解包
const object = {
  id: ref(1) // 这就是一个非顶级 Ref 不会自动解包
}
</script>

<style lang="scss" scoped></style>

例如我们在模板中各自加 1 就会发现上面因为已经解包出来了,所以能够正常的进行表达式的计算。

但是下面因为没有解包,意味着 object.id 仍然是一个对象,因此最终计算的结果为 [object Object]1

因此要访问 object.id 的值,没有自动解包我们就手动访问一下 value

<template>
  <div>
    <div>{{ count + 1 }}</div>
    <div>{{ object.id.value + 1 }}</div>
  </div>
</template>
最近更新:: 2025/7/9 07:29
Contributors: AK
Prev
模板语法
Next
响应式常用 API