Skip to content

Vue

Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

Vue 的两个核心功能

声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。

响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

一、单文件组件

也被称为*.vue文件,英文 Single-File Components,缩写为SFC。Vue 的单文件组件会将一个组件的逻辑(JavaScript),模板(HTML)和样式(CSS)封装在同一个文件里。

二、API 风格

Vue 的组件可以按两种不同的风格书写:选项式 API组合式 API

选项式 API

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如datamethodsmounted。选项所定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例

组合式 API

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用

如何选取 API 风格

  • 选项式 API 以组件实例的概念为中心,即this关键字指向组件实例。它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
  • 组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题,这种形式更加自由,也需要开发者对 vue 的响应式系统有更深的理解才能高效使用。

三、vue 的生命周期

以下是 Vue 生命周期的主要阶段及要点总结:

  1. beforeCreate: 实例刚被创建,此时数据观测、事件机制等都未初始化,无法访问到 data 中的数据、methods 里的方法。

  2. created: 实例创建完成,数据观测、事件机制已配置好,可以访问 data 和 methods 中的内容,但尚未开始挂载到 DOM,不能获取到 DOM 元素。

  3. beforeMount: 在挂载开始之前被调用,相关的渲染函数首次被调用,虚拟 DOM 已经创建,不过真实 DOM 还未生成及替换。

  4. mounted: 实例挂载完成,真实 DOM 已被创建并替换掉了页面中对应的元素,此时可以操作 DOM 节点了,一般在这里进行 DOM 相关的初始化操作。

  5. beforeUpdate: 数据发生变化时会触发,此时 DOM 还未更新,可在这个阶段进行一些数据更新前的准备工作。

  6. updated: DOM 已根据数据变化更新完毕,避免在此阶段进行会再次触发更新的数据修改,不然可能陷入死循环。

  7. beforeDestroy: 实例销毁之前调用,此时实例仍然可用,可以做一些清理工作,比如清除定时器、解绑事件监听等。

  8. destroyed: 实例销毁完成,所有的绑定、监听器等都已被移除,组件相关的所有东西都已解除关联。

四、Vuex 底层实现原理

响应式原理的应用

Vuex 的 state 是响应式的。其底层利用了 Vue.js 的响应式系统,通过 Object.defineProperty()或者 Proxy(在 Vue 3 中主要使用)来进行数据劫持。例如,当一个组件访问 this.$store.state 中的某个属性时,实际上是访问经过响应式处理后的属性。 当 state 中的属性被修改时,会触发相应的更新机制。这个更新机制和 Vue 组件的更新机制类似,会通知到依赖这个 state 的组件进行重新渲染。

Mutation 的实现机制

Mutation 本质上是一些同步函数,这些函数被定义在一个对象中。当一个 Mutation 被提交时,实际上是在调用这个对象中的一个函数。 在底层,每次提交 Mutation 都会在严格模式下进行记录,这样可以方便地追踪状态变化的历史,有助于调试复杂的状态变化。

Action 的异步处理方式

Action 主要用于处理异步操作。它返回一个 Promise,可以在内部使用 axios 等工具来进行网络请求等异步操作。 底层在执行 Action 时,会等待异步操作完成后,通过调用 commit 方法来提交 Mutation,从而间接修改 state,这样就保证了状态的修改仍然是通过 Mutation 来进行的。

Getter 的计算机制

Getter 的底层实现类似于 Vue 中的计算属性。它会缓存计算结果,当它所依赖的 state 属性发生变化时,会重新计算。 利用了 JavaScript 的闭包特性,在定义 Getter 的时候,它会捕获其依赖的 state 属性,并且会在 state 变化时自动重新计算并返回最新的结果。

五、nextTick 原理

核心:Vue 的 DOM 更新是异步批量执行的,nextTick 让你在 DOM 更新后执行回调。

底层机制:

  1. Vue 修改数据后,不会立即更新 DOM,而是把更新任务放进一个异步队列(微任务)。

  2. nextTick 会把你的回调也放进这个队列,确保在 DOM 更新后执行。

  3. 浏览器执行微任务(如 Promise.then、MutationObserver)时,按顺序执行队列里的任务。

面试回答技巧

nextTick 的原理是什么?

答:

Vue 的 DOM 更新是异步的,数据变化后不会立即渲染,而是把更新任务放进微任务队列。
nextTick 会把回调也放进这个队列,确保在 DOM 更新后执行。
底层基于 Promise.then(Vue 3)或降级到 setTimeout(兼容老浏览器)。

举例

比如我修改了 data,想立刻获取更新后的 DOM 高度,就必须用 nextTick,否则拿到的还是旧值。

六、动态参数

在指令参数上可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

html
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

相似地,你还可以将一个函数绑定到动态的事件名称上:

html
<a v-on:[eventName]="doSomething"> ... </a>

<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>

在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus。

七、修饰符 Modifiers

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。

例如

html
<button @click.stop="handleClick">点击不冒泡</button>

<form @submit.prevent="onSubmit">阻止提交</form>

<!-- 后续点击完全无效(相当于移除了事件监听)
适用场景:表单提交防止重复提交 -->
<button @click.once="handleClick">仅生效一次</button>

<div @click.capture="handleCapture">捕获阶段触发</div>

<!-- (忽略子元素冒泡上来的事件) -->
<div @click.self="handleSelf">仅自身点击有效</div>

<div @scroll.passive="onScroll">平滑滚动</div>

<!-- (Vue 2):监听组件根元素的原生事件 -->
<my-component @click.native="handleNativeClick"></my-component>

在这里你可以直观地看到完整的指令语法:

vue_directive.png

八、ref 和 reactive

  1. ref:
    • 用于包装基本类型值(如字符串、数字、布尔值)
    • 需要通过 .value 访问和修改值
    • 在模板中自动解包,不需要 .value
  2. reactive:
    • 用于包装对象类型值(如数组、对象)
    • 不需要 .value 访问
    • 可以直接修改对象属性

示例

javascript
import { ref, reactive } from "vue";

// ref 示例
const count = ref(0);
console.log(count.value); // 访问值
count.value++; // 修改值

// reactive 示例
const user = reactive({
  name: "张三",
  age: 20,
});
console.log(user.name); // 访问属性
user.age = 21; // 修改属性

何时使用

  • 用 ref 当你的数据是基本类型或需要重新赋值的引用类型
  • 用 reactive 当你的数据是对象/数组且不需要整体替换

    提示:在组合式 API 中,大多数情况下用 ref 会更灵活,因为可以处理所有类型的数据。

九、可写计算属性

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

vue
<script setup>
import { ref, computed } from "vue";

const firstName = ref("John");
const lastName = ref("Doe");

const fullName = computed({
  // getter
  get() {
    return firstName.value + " " + lastName.value;
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(" ");
  },
});
</script>

十、scoped 样式的原理

核心原理

  1. ​HTML 属性标记 ​:
    • 为当前组件的每个 DOM 元素添加一个 ​ 唯一的 data 属性 ​(如 data-v-123456)
  2. ​CSS 属性选择器 ​:
    • 为组件内的每个 CSS 选择器添加对应的属性选择器(如 .button → .button[data-v-123456])

深度选择器

当需要影响子组件样式时:

css
/* 旧语法 */
::v-deep .child-component {
  color: red;
}
/* 或 /deep/、>>> (已废弃) */

/* Vue 3 推荐语法 */
:deep(.child-component) {
  color: red;
}

注意事项

  1. ​ 不影响全局样式​:带有 scoped 的样式不会影响其他组件
  2. ​ 性能影响​:属性选择器比普通类选择器稍慢,但影响很小
  3. ​ 不适用于动态生成的内容 ​:通过 v-html 添加的内容不会被添加 data 属性

实现机制

  1. ​ 编译阶段 ​:Vue 的模板编译器会为组件生成唯一哈希
  2. ​PostCSS 处理 ​:使用 postcss-modules-scope 插件转换 CSS
  3. ​ 运行时 ​:Vue 将处理后的 CSS 注入到页面中

十一、$attrs 的使用

$attrs 是 Vue 组件的一个特殊属性,包含了父组件传递给子组件但未被 props 显式声明的所有 attribute 绑定。

示例

vue
<template>
  <!-- MyComponent.vue 多根模板 -->
  <p :class="$attrs.class">Hi!</p>
  <span>This is a child component</span>
</template>

<!-- 父组件使用 -->
<MyComponent class="baz" />

通过 v-bind="$attrs" 传入内部组件:

vue
<template>
  <div>
    <child-component v-bind="$attrs"></child-component>
  </div>
</template>

十二、v-if 和 v-show

v-if是一个指令,它必须依附于某个元素,如果想要切换不止一个元素,可以在一个<template>元素上使用v-if

vue
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

vue
<h1 v-show="ok">Hello!</h1>

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行(一般不推荐这样做)。

十三、v-for 的用法

js
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
vue
<li v-for="item in items">
  {{ item.message }}
</li>

v-for 还支持一个可选的第二个参数为当前项的索引:

js
const parentMessage = ref("Parent");
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
vue
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

实际上,你也可以在定义v-for的变量别名时使用解构

vue
<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.values() 的返回值来决定。

js
const myObject = reactive({
  title: "How to do lists in Vue",
  author: "Jane Doe",
  publishedAt: "2016-04-10",
});
vue
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

可以通过提供第二个参数表示属性名 (例如 key):

vue
<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>

第三个参数表示位置索引:

vue
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

v-for里使用范围值

v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

vue
<span v-for="n in 10">{{ n }}</span>

注意此处 n 的初值是从 1 开始而非 0。


v-forv-if

当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

vue
<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

在外先包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

vue
<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

十四、事件处理

在内联事件处理器中访问事件参数

有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

vue
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>
js
function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault();
  }
  alert(message);
}

按键修饰符

vue
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />

.exact 修饰符

.exact 修饰符允许精确控制触发事件所需的系统修饰符的组合。

vue
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<!-- 原理:检查点击事件event中的ctrlKey、shiftKey、altKey、metaKey是否为false -->
<!-- 仅当所有属性均为 false 时,才触发绑定的方法。 -->
<button @click.exact="onClick">A</button>

十五、表单输入绑定

.lazy

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

vue
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

区别:

  • 默认行为:每次输入框的 input 事件触发时(即用户每输入一个字符)立即同步数据。
  • 使用 .lazy 修饰符:仅在输入框失去焦点(change 事件)时同步数据。

十六、侦听器

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

js
const x = ref(0);
const y = ref(0);

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`);
});

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`);
  }
);

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`);
});

注意,你不能直接侦听响应式对象的属性值,例如:

js
const obj = reactive({ count: 0 });

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`);
});

这里需要用一个返回该属性的 getter 函数:

js
// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`);
  }
);

Post Watchers

如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:

js
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

停止侦听器

侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。

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

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:

js
const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

十七、组件上的ref

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。

有一个例外的情况,使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

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

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

注意

defineExpose 必须在任何 await 操作 之前 调用。否则,在 await 操作后暴露的属性和方法将无法访问。