Skip to content

一、Vue 基础 ​

1. Vue的基本原理 ​

Vue 是一款 MVVM(Model-View-ViewModel) 框架,它的核心原理是 数据驱动视图,实现了 双向数据绑定 和 组件化开发。Vue 主要依赖 响应式数据系统 和 虚拟 DOM 来实现高效的界面更新。

Vue 的核心流程包括:

  1. 初始化 Vue 实例
  2. 解析模板并转换为虚拟 DOM
  3. 建立数据响应式(reactive() / defineProperty)
  4. 进行依赖收集(Watcher 监听数据变化)
  5. 数据更新后,触发 diff 算法对比虚拟 DOM
  6. 最小化真实 DOM 变更,提高性能

当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。 0_tB3MJCzh_cB6i3mS-1.png

2. 双向数据绑定的原理 ​

Vue 的双向数据绑定是通过 数据劫持(Data Hijacking) 和 发布-订阅模式(Observer Pattern) 实现的,核心原理主要依赖于 Object.defineProperty(Vue 2)或 Proxy(Vue 3)。以下是具体的实现原理:


Vue 2.x 双向数据绑定原理 ​

Vue 2 主要利用 Object.defineProperty() 进行数据劫持,并结合 观察者模式 来实现视图和数据的同步。

1. 关键组件 ​

(1) Observer(数据劫持) ​
  • 使用 Object.defineProperty() 递归地对对象的每个属性进行 getter 和 setter 劫持。
  • 当数据发生变化时,通知 Watcher 触发视图更新。
js
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取 ${key}:`, val);
      return val;
    },
    set(newVal) {
      console.log(`设置 ${key}:`, newVal);
      val = newVal; // 这里会触发 DOM 更新
    }
  });
}

let data = { message: "Hello Vue" };
defineReactive(data, "message", data.message);

console.log(data.message); // 触发 get
data.message = "Hello World"; // 触发 set
(2) Dep(依赖收集) ​
  • 依赖管理器 Dep 负责收集依赖(Watcher),当数据变化时,通知所有依赖更新。
  • 每个 响应式属性 都有一个 Dep 实例,它用于管理依赖的 Watcher。
js
class Dep {
  constructor() {
    this.subs = []; // 存储所有的 Watcher
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
(3) Watcher(观察者) ​
  • Watcher 是 Vue 的核心之一,每个绑定在 模板 上的指令或表达式都会创建一个 Watcher 。
  • 当数据变化时,Watcher 会触发 视图更新(update)。
js
class Watcher {
  constructor(obj, key, cb) {
    this.obj = obj;
    this.key = key;
    this.cb = cb;
    Dep.target = this;
    this.value = obj[key]; // 触发 getter
    Dep.target = null;
  }
  update() {
    let newValue = this.obj[this.key];
    this.cb(newValue);
  }
}
(4) Compile(模板编译) ​
  • 解析 Vue 模板中的 {{}} 语法,并替换成数据,同时建立 Watcher 监听数据变化。
js
function compile(node, vm) {
  let reg = /\{\{(.*)\}\}/;
  if (node.nodeType === 3 && reg.test(node.nodeValue)) {
    let key = RegExp.$1.trim();
    new Watcher(vm, key, function(newVal) {
      node.nodeValue = newVal;
    });
    node.nodeValue = vm[key]; // 初始化数据
  }
}
(5) Vue 实例的初始化 ​
js
class Vue {
  constructor(options) {
    this.$data = options.data;
    Object.keys(this.$data).forEach(key => {
      defineReactive(this.$data, key, this.$data[key]);
    });
    options.el && compile(document.querySelector(options.el), this);
  }
}

Vue 2.x 双向绑定的完整流程 ​

  1. Observer 通过 Object.defineProperty 劫持 data 的每个属性,添加 getter / setter。
  2. Watcher 在 getter 过程中 收集依赖(Dep)。
  3. 当数据变化时,触发 setter,然后 Dep.notify() 通知所有 Watcher,执行 update 更新视图。

Vue 3.x 双向数据绑定原理 ​

Vue 3 采用 Proxy 代替 Object.defineProperty,具有更强的能力:

Vue 3 关键点 ​

  1. 使用 Proxy 代理整个对象,而不是遍历对象的每个属性。
  2. 支持数组和对象的动态添加/删除(Vue 2 需要 Vue.set())。
  3. 更高的性能,避免 defineProperty 递归的问题。
js
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log(`获取 ${key}:`, target[key]);
      return target[key];
    },
    set(target, key, value) {
      console.log(`设置 ${key}:`, value);
      target[key] = value;
      return true;
    }
  });
}

let data = reactive({ message: "Hello Vue 3" });
console.log(data.message); // 触发 get
data.message = "Hello Proxy"; // 触发 set

Vue 3.x 双向绑定完整流程 ​

  1. reactive() 通过 Proxy 劫持 data,并监听所有属性访问(get)和修改(set)。
  2. effect() 作为 Watcher 监听数据变化,执行 update 触发视图更新。
  3. ref() 和 computed() 也是基于 reactive() 进一步封装的响应式 API。

Vue 2 vs Vue 3 双向绑定对比 ​

Vue 2 (Object.defineProperty)Vue 3 (Proxy)
性能需要递归遍历所有属性直接代理整个对象,性能更优
数组监听不能监听索引变化,需 Vue.set()原生支持
新增属性不能监听新属性可以监听
对象深层监听需要 Vue.set() 手动处理默认深层监听

总结 ​

  • Vue 2 使用 Object.defineProperty() 进行 数据劫持,Vue 3 则使用 Proxy 提供更高效的响应式系统。
  • Vue 2 需要 手动递归遍历对象属性,而 Vue 3 直接 代理整个对象,性能更好。
  • Vue 3 响应式 API 更灵活,如 reactive()、ref()、computed() 让开发更加直观。

Vue 3 的 Proxy 方案是未来的发展方向,解决了 Vue 2 的一些 性能和维护成本问题,推荐在新项目中使用!🚀

3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点? ​

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

4. MVVM、MVC、MVP的区别 ​

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。

(1)MVC ​

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。 image.png

(2)MVVM ​

MVVM 分为 Model、View、ViewModel:

  • Model代表数据模型,数据和业务逻辑都在Model层中定义;
  • View代表UI视图,负责数据的展示;
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。 image.png

(3)MVP ​

MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

5. Computed、Methods 和 Watch 的区别 ​

Vue 提供了 computed(计算属性)、methods(方法)和 watch(监听器)三种方式来处理数据,虽然它们有相似的功能,但用途和底层机制不同。

对比项computed(计算属性)methods(方法)watch(监听器)
适用场景基于已有数据计算新值执行某个操作(不涉及依赖数据)监听数据变化后执行操作
是否有缓存✅ 有缓存(依赖数据不变不会重新计算)❌ 无缓存(每次调用都会执行)❌ 无缓存(数据变化时,每次都会执行)
返回值返回计算后的值返回执行结果不返回值,执行副作用(如请求、DOM 操作)
是否适用于异步❌ 不适用✅ 适用✅ 适用(最推荐)
是否依赖响应式数据✅ 依赖响应式数据❌ 不依赖,手动调用✅ 依赖,监听数据变化
执行时机依赖数据变化时自动重新计算只有在手动调用时执行监听到数据变化后执行

1. computed(计算属性) ​

特点

  • 基于响应式数据计算新值
  • 有缓存(如果依赖的数据不变,不会重复计算)
  • 适用于同步计算,不适用于异步操作

示例

js
import { ref, computed } from 'vue';

const price = ref(100);
const quantity = ref(2);

// 计算总价(有缓存)
const totalPrice = computed(() => price.value * quantity.value);

console.log(totalPrice.value); // 200

price.value = 150;
console.log(totalPrice.value); // 300 (重新计算)

✅ 适用场景

  • 需要基于多个响应式数据 计算派生值。
  • 避免不必要的重复计算,提高性能。

2. methods(方法) ​

特点

  • 没有缓存,每次调用都会重新执行。
  • 适用于执行某些操作(如事件响应、API 调用)。
  • 手动调用,不会自动依赖响应式数据。

示例

js
import { ref } from 'vue';

const price = ref(100);
const quantity = ref(2);

// 计算总价(每次调用都执行)
function getTotalPrice() {
  return price.value * quantity.value;
}

console.log(getTotalPrice()); // 200

price.value = 150;
console.log(getTotalPrice()); // 300 (每次调用都会计算)

✅ 适用场景

  • 计算结果不需要缓存,每次调用都要执行。
  • 需要手动触发的操作,如 事件响应、按钮点击。

3. watch(监听器) ​

特点

  • 监听数据变化后执行回调(适用于副作用,如 API 请求)。
  • 适用于异步操作(computed 不支持异步)。
  • 可以监听单个值、多个值或整个对象(深度监听)。

示例 1:监听单个 ref 变化

js
import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newVal, oldVal) => {
  console.log(`count 变化: ${oldVal} -> ${newVal}`);
});

count.value = 5; // 触发 watch

输出:

count 变化: 0 -> 5

示例 2:监听对象(深度监听)

js
import { reactive, watch } from 'vue';

const user = reactive({ name: "Alice", age: 25 });

watch(user, (newVal, oldVal) => {
  console.log("user 发生变化:", newVal);
}, { deep: true });

user.age = 26; // 触发 watch

示例 3:监听数据变化后调用 API

js
watch(count, async (newVal) => {
  const res = await fetch(`https://api.example.com/data?id=${newVal}`);
  console.log(await res.json());
});

✅ 适用场景

  • 执行异步请求、定时任务、手动 DOM 操作(computed 不能处理)。
  • 监听数据变化后触发回调(适用于副作用)。
  • 深度监听对象的变化。

4. computed vs methods vs watch ​

🚀 什么时候用 computed? ​

✅ 当一个值依赖于 其他响应式数据计算,且希望 缓存计算结果 时:

js
const fullName = computed(() => user.firstName + " " + user.lastName);

🚀 什么时候用 methods? ​

✅ 当你需要 触发某个操作,而不是依赖数据的自动更新:

js
function fetchData() {
  console.log("Fetching data...");
}

⛔ 不要用 methods 替代 computed,否则会导致 每次调用都重新计算。


🚀 什么时候用 watch? ​

✅ 当你需要 监听数据变化并执行副作用(如 API 请求):

js
watch(count, async (newVal) => {
  const res = await fetchData(newVal);
  console.log(res);
});

⛔ 不要用 watch 替代 computed,因为 watch 不会返回计算结果。


5. 总结 ​

computedmethodswatch
适用场景计算派生值事件触发、执行操作监听数据变化,执行副作用
是否有缓存✅ 有缓存❌ 无缓存❌ 无缓存
是否自动执行✅ 依赖数据变化时自动执行❌ 手动调用✅ 监听到数据变化时执行
是否适用于异步❌ 不适用✅ 适用✅ 适用
是否返回值✅ 返回计算结果✅ 返回方法执行结果❌ 不返回值

🚀 选择指南 ​

  1. 需要缓存的计算数据 → computed
  2. 需要手动触发的操作(如按钮点击) → methods
  3. 监听数据变化并执行副作用(如 API 请求) → watch

✅ 最佳实践

js
import { ref, computed, watch } from 'vue';

const price = ref(100);
const quantity = ref(2);

// ✅ 用 computed 计算派生值(有缓存)
const totalPrice = computed(() => price.value * quantity.value);

// ✅ 用 methods 触发操作
function updatePrice() {
  price.value += 10;
}

// ✅ 用 watch 监听数据变化并执行副作用
watch(price, (newVal, oldVal) => {
  console.log(`价格变化: ${oldVal} -> ${newVal}`);
});

updatePrice(); // price.value 变为 110,触发 watch
console.log(totalPrice.value); // 220

>[!tip] 🚀 一句话总结 > computed 用于计算新值,methods 用于执行操作,watch 用于监听变化执行副作用! 🎯

6. slot是什么?有什么作用?原理是什么? ​

1. 什么是 slot? ​

slot 是 Vue 提供的一种 内容分发机制,用于在 父组件传递内容到子组件的指定位置。
它本质上是 占位符,在 子组件模板中定义,父组件传递内容填充。

2. slot 的作用 ​

  • 让父组件可以向子组件传递 HTML 内容或其他 Vue 组件。
  • 使组件更加灵活和可复用,不局限于固定结构。

3. slot 的基本使用 ​

🔹 例子:基础插槽 ​

父组件:

vue
<template>
  <ChildComponent>
    <p>这段文本来自父组件</p>
  </ChildComponent>
</template>

子组件:

vue
<template>
  <div>
    <slot></slot> <!-- 这里会插入父组件的内容 -->
  </div>
</template>

🔹 结果(最终渲染的 HTML)

html
<div>
  <p>这段文本来自父组件</p>
</div>

📌 解释:

  • 子组件 ChildComponent 内部定义了 <slot>。
  • 父组件的 <p>这段文本来自父组件</p> 会填充进 <slot> 位置。

4. slot 的三种类型 ​

Vue 提供了 三种插槽类型:

  1. 默认插槽(Default Slot)
  2. 具名插槽(Named Slot)
  3. 作用域插槽(Scoped Slot)

4.1 默认插槽 ​

📌 作用:

  • 直接用 <slot></slot>,父组件的所有内容都会被插入。
示例 ​

子组件:

vue
<template>
  <div>
    <h2>我是子组件</h2>
    <slot></slot>  <!-- 默认插槽 -->
  </div>
</template>

父组件:

vue
<template>
  <ChildComponent>
    <p>来自父组件的内容</p>
  </ChildComponent>
</template>

🔹 渲染结果

html
<div>
  <h2>我是子组件</h2>
  <p>来自父组件的内容</p>
</div>

4.2 具名插槽(Named Slot) ​

📌 作用:

  • 在子组件中使用多个插槽,并给它们命名,父组件可以指定内容插入到哪个插槽。
示例 ​

· 子组件: </ChildComponent>

vue
<template>
  <div>
    <slot name="header"></slot>  <!-- 头部插槽 -->
    <p>我是子组件的内容</p>
    <slot name="footer"></slot>  <!-- 底部插槽 -->
  </div>
</template>

父组件:

vue
<template>
  <ChildComponent>
    <template v-slot:header>
      <h1>我是 Header</h1>
    </template>
    <template v-slot:footer>
      <p>我是 Footer</p>
    </template>
  </ChildComponent>
</template>

🔹 渲染结果

html
<div>
  <h1>我是 Header</h1>
  <p>我是子组件的内容</p>
  <p>我是 Footer</p>
</div>

4.3 作用域插槽(Scoped Slot) ​

📌 作用:

  • 让子组件把数据传递给父组件的插槽,父组件可以动态使用子组件的数据。

示例 ​

子组件:

vue
<template>
  <div>
    <slot :user="user"></slot>  <!-- 向插槽传递 `user` 数据 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: { name: "Alice", age: 25 }
    };
  }
};
</script>

父组件:

vue
<template>
  <ChildComponent>
    <template v-slot:default="slotProps">
      <p>用户名: {{ slotProps.user.name }}</p>
      <p>年龄: {{ slotProps.user.age }}</p>
    </template>
  </ChildComponent>
</template>

🔹 渲染结果

html
<div>
  <p>用户名: Alice</p>
  <p>年龄: 25</p>
</div>

📌 解释:

  • 子组件 ChildComponent 通过 slot 传递数据 { user: { name: "Alice", age: 25 } }。
  • 父组件的 v-slot:default="slotProps" 读取数据,并渲染 slotProps.user.name 和 slotProps.user.age。

5. slot 的底层原理 ​

Vue 在渲染 slot 时,使用了 虚拟 DOM + 作用域解析 来实现内容插入:

  1. 编译阶段:

    • Vue 解析 <slot>,生成虚拟 DOM 占位符。
    • 具名插槽的 name 作为 唯一标识,匹配父组件内容。
    • 作用域插槽的数据通过 函数传递 给父组件。
  2. 渲染阶段:

    • Vue 在渲染父组件时,将 slot 里的内容替换到子组件对应的 <slot> 位置。

Vue 内部会将 slot 转换成 render 函数:

js
// 具名插槽的原理
function render() {
  return h("div", [
    this.$slots.header(),
    h("p", "我是子组件的内容"),
    this.$slots.footer()
  ]);
}

6. slot 的优点 ​

✅ 让组件更加灵活,支持不同内容的动态传递。
✅ 提高组件复用性,组件可以适应不同的需求,而不需要修改内部代码。
✅ 支持作用域插槽,子组件可以给父组件提供数据,增强数据传递能力。


7. 总结 ​

类型作用语法示例
默认插槽插入普通内容<slot></slot>
具名插槽多个插槽,可按名称插入内容<slot name="header"></slot>
作用域插槽子组件提供数据给父组件<slot :data="user"></slot>

👉 slot 是 Vue 组件化开发的核心技术,使得组件更具扩展性和灵活性! 🚀

7. 如何保存页面的当前的状态 ​

在 Vue 应用中,要 保存页面的当前状态,可以使用以下几种方法:


1. Vue2 组件的 data(短暂状态,刷新后丢失) ​

适用于:组件内部的状态管理

  • 组件的 data 变量可以存储临时状态,但 刷新页面时会丢失。

示例:

vue
<template>
  <div>
    <p>当前计数: {{ count }}</p>
    <button @click="count++">增加</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 }; // 页面刷新后会重置
  }
};
</script>

📌 缺点:页面刷新后状态会丢失


2. Vuex / Pinia(全局状态管理,刷新后丢失,手动存储可保持) ​

适用于:全局状态管理,如登录信息、用户偏好设置

  • Vue 2 推荐使用 Vuex
  • Vue 3 推荐使用 Pinia

示例:使用 Pinia

sh
npm install pinia
js
// store.js
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
    }
  }
});
vue
<template>
  <div>
    <p>计数: {{ counter.count }}</p>
    <button @click="counter.increment()">增加</button>
  </div>
</template>

<script>
import { useCounterStore } from "@/store";
import { storeToRefs } from "pinia";

export default {
  setup() {
    const counter = useCounterStore();
    return { counter: storeToRefs(counter) };
  }
};
</script>

📌 缺点:Vuex/Pinia 默认状态存储在内存中,页面刷新会丢失。

📌 解决方案:可以结合 localStorage/sessionStorage 持久化存储。


3. localStorage(刷新不丢失,页面关闭后仍然存** ​

适用于:长期存储,如用户设置、Token、购物车数据

  • localStorage 数据不会随页面关闭而丢失,除非手动清除。

示例:

vue
<template>
  <div>
    <p>当前计数: {{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 };
  },
  mounted() {
    // 读取 localStorage 数据
    const savedCount = localStorage.getItem("count");
    if (savedCount) {
      this.count = parseInt(savedCount);
    }
  },
  methods: {
    increment() {
      this.count++;
      localStorage.setItem("count", this.count); // 保存到 localStorage
    }
  }
};
</script>

📌 优点:刷新页面后数据仍然存在。
📌 缺点:需要手动存储数据,不能监听数据变化。


4. sessionStorage(刷新不丢失,关闭浏览器或标签页后清除) ​

适用于:短期存储,如表单临时数据

  • sessionStorage 只在当前标签页有效,关闭浏览器/标签页后数据清除。

示例:

vue
methods: {
  saveData() {
    sessionStorage.setItem("formData", JSON.stringify(this.formData));
  }
}

📌 适用于表单数据存储,防止用户误刷新丢失内容。


5. keep-alive(组件缓存,不刷新页面时保持状态) ​

页面刷新后 keep-alive 缓存会丢失,因为它存储在 JavaScript 内存 中(浏览器关闭或刷新时会清空)

适用于:切换路由时,保持组件状态

  • Vue 提供 <keep-alive> 组件,防止组件销毁后丢失数据(仅适用于 data 变量)。

示例:

vue
<template>
  <keep-alive>
    <router-view></router-view>
  </keep-alive>
</template>

📌 适用于:

  • 切换路由时保持页面状态,如分页、搜索结果等。

6. IndexedDB(大数据存储) ​

适用于:存储大量数据,如离线数据、缓存

  • IndexedDB 提供比 localStorage 更大、更结构化的数据存储。

示例:

js
let db;
const request = indexedDB.open("myDatabase", 1);
request.onsuccess = function(event) {
  db = event.target.result;
};

📌 适用于离线存储,但比 localStorage 复杂。


7. 结合 Vuex/Pinia + localStorage 持久化 ​

适用于:在 Vuex/Pinia 里自动持久化状态 示例:Pinia 持久化

js
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: JSON.parse(localStorage.getItem("count")) || 0 }),
  actions: {
    increment() {
      this.count++;
      localStorage.setItem("count", JSON.stringify(this.count));
    }
  }
});

📌 优点:

  • Vuex/Pinia 管理状态
  • localStorage 持久化数据,刷新后恢复状态

8. 总结:如何选择? ​

方式适用场景刷新后状态是否保持适用数据类型
data组件内部状态❌ 丢失临时状态
Vuex / Pinia全局状态管理❌ 丢失(需持久化)用户数据、全局配置
localStorage长期存储✅ 保持用户设置、购物车
sessionStorage页面存储✅(仅当前页面)表单数据
keep-alive组件缓存✅(页面未刷新)组件数据
IndexedDB大数据存储✅ 保持离线数据

推荐方案 ​

✅ 短暂数据(页面状态) → data、keep-alive
✅ 跨组件状态(全局共享) → Vuex/Pinia
✅ 刷新后持久化状态 → localStorage + Vuex/Pinia
✅ 仅当前标签页有效的数据 → sessionStorage
✅ 存储大量数据 → IndexedDB


💡 总结 ​

  • 如果只是组件内的状态,直接用 data。
  • 如果多个组件共享状态,用 Vuex/Pinia,并结合 localStorage 持久化。
  • 如果需要保存状态到本地(页面刷新不丢失),用 localStorage 或 sessionStorage。
  • 如果仅在路由切换时保持状态,用 keep-alive。

🚀 最佳实践:Pinia + LocalStorage,既能管理状态,又能持久化存储!

8. 常见的事件修饰符及其作用 ​

Vue 提供了一些 事件修饰符,用于简化事件处理,防止默认行为或事件冒泡等问题。修饰符通过 .(点) 方式添加到 v-on (@) 事件监听器后面,例如:@click.stop。


1. 常见事件修饰符 ​

修饰符作用示例
.stop阻止事件冒泡@click.stop="handleClick"
.prevent阻止默认事件(如表单提交、链接跳转)@submit.prevent="handleSubmit"
.self只在事件本身触发时生效(不冒泡)@click.self="handleClick"
.once事件只触发一次@click.once="handleClick"
.capture使用捕获模式触发事件(优先于子元素触发)@click.capture="handleClick"
.passive优化滚动事件,提升性能@scroll.passive="handleScroll"

2. 事件修饰符的详细解析 ​

(1) .stop —— 阻止事件冒泡 ​

📌 作用: 防止事件冒泡到父元素。

示例

vue
<template>
  <div @click="parentClick">
    <button @click.stop="childClick">点击我</button>
  </div>
</template>

<script>
export default {
  methods: {
    parentClick() {
      console.log("父元素点击");
    },
    childClick() {
      console.log("子元素点击");
    }
  }
};
</script>

📌 点击 <button> 只会打印 "子元素点击",不会触发 "父元素点击"。
不加 .stop,点击按钮时会触发 div 的 click 事件(事件冒泡)。


(2) .prevent —— 阻止默认行为 ​

📌 作用: 阻止浏览器的默认事件,如 表单提交、超链接跳转。

示例

vue
<form @submit.prevent="handleSubmit">
  <button type="submit">提交</button>
</form>
js
methods: {
  handleSubmit() {
    console.log("提交事件被阻止");
  }
}

📌 点击按钮不会触发表单的默认提交。


(3) .self —— 仅限自身触发 ​

📌 作用: 只有事件目标是自身时才触发,阻止子元素触发事件。

示例

vue
<div @click.self="handleClick">
  <button>点击不会触发父级</button>
</div>
js
methods: {
  handleClick() {
    console.log("只有点击 div 本身时才触发");
  }
}

📌 点击 button 不会触发 div 的 click 事件,点击 div 空白区域才会触发。


(4) .once —— 事件只触发一次 ​

📌 作用: 事件处理函数只执行一次,之后的点击不会再触发。

示例

vue
<button @click.once="handleClick">点我只执行一次</button>
js
methods: {
  handleClick() {
    console.log("我只执行一次");
  }
}

📌 第一次点击打印 "我只执行一次",后续点击无效。


(5) .capture —— 事件捕获模式 ​

📌 作用: 让父级元素先触发事件,然后才是子元素(事件捕获)。

示例

vue
<div @click.capture="parentClick">
  <button @click="childClick">点击我</button>
</div>
js
methods: {
  parentClick() {
    console.log("父元素先触发");
  },
  childClick() {
    console.log("子元素后触发");
  }
}

📌 点击 button 会先打印 "父元素先触发",再打印 "子元素后触发"(默认情况是子元素先触发)。


(6) .passive —— 提高滚动性能 ​

📌 作用: 提高滚动性能,让 scroll 事件变为非阻塞(默认 event.preventDefault() 不能使用)。

示例

vue
<div @scroll.passive="handleScroll">滚动区域</div>
js
methods: {
  handleScroll() {
    console.log("滚动触发");
  }
}

📌 适用于 touchmove、wheel 事件,优化移动端滚动体验。
📌 如果 passive,则 preventDefault() 无法阻止默认行为。


3. 事件修饰符组合使用 ​

多个修饰符可以组合使用,但 顺序很重要。

组合修饰符作用
.stop.prevent先阻止事件冒泡,再阻止默认行为
.capture.once事件捕获模式,且只触发一次
.self.prevent只在自身触发时阻止默认行为

示例 ​

vue
<button @click.stop.prevent="handleClick">点击我</button>

📌 点击时不会冒泡,同时也阻止默认行为(如表单提交)。


4. 事件修饰符 VS JavaScript ​

事件修饰符可以用 JavaScript 代码替代:

Vue 修饰符JavaScript 等价代码
@click.stopevent.stopPropagation()
@submit.preventevent.preventDefault()
@click.selfif (event.target === event.currentTarget) {...}
@click.onceel.addEventListener("click", handler, { once: true })
@scroll.passiveel.addEventListener("scroll", handler, { passive: true })

5. 总结 ​

修饰符作用适用场景
.stop阻止事件冒泡避免子元素触发父级事件
.prevent阻止默认行为阻止表单提交、链接跳转
.self只在自身触发时执行过滤掉子元素触发的事件
.once事件只触发一次只需执行一次的点击事件
.capture事件捕获模式(先执行父级)让父级事件先触发
.passive提高滚动性能scroll、touchmove 事件

🚀 最佳实践

  • 阻止默认行为 → @submit.prevent
  • 防止事件冒泡 → @click.stop
  • 优化滚动性能 → @scroll.passive
  • 避免多次触发 → @click.once
  • 监听自身事件,避免子元素影响 → @click.self

🎯 选择正确的修饰符可以减少代码量,提高代码可读性,优化性能! 🚀

9. v-if、v-show、v-html 的原理 ​

Vue 提供了 v-if、v-show 和 v-html 指令,分别用于 条件渲染、显示/隐藏 和 动态渲染 HTML。它们的原理和使用方式各不相同,下面详细介绍它们的区别和底层原理。


1. v-if 的原理 ​

🔹 作用

  • 按条件渲染元素(当条件为 false 时,DOM 完全删除)。
  • 适用于 不需要频繁切换的场景(比如:权限控制、路由跳转)。

🔹 语法

vue
<p v-if="show">显示内容</p>
<p v-else>隐藏内容</p>
vue
<p v-if="score >= 90">优秀</p>
<p v-else-if="score >= 60">及格</p>
<p v-else>不及格</p>

🔹 v-if 的底层原理 ​

v-if 在 Vue 编译模板时,会转换成 条件判断 + 虚拟 DOM 操作:

js
render() {
  return this.show ? h("p", "显示内容") : h("p", "隐藏内容");
}
  • 当 show 变为 false,Vue 完全删除 <p> 元素,不会出现在 DOM 中。
  • 组件的生命周期钩子(created、mounted 等)会随 v-if 的销毁和创建执行。

🔹 v-if 的特点 ​

✅ 真正的 DOM 操作(createElement() / removeChild())。
✅ 适用于开销较大的组件,减少不必要的渲染。
❌ 频繁切换时,性能较低(因涉及 DOM 操作)。


2. v-show 的原理 ​

🔹 作用

  • 控制元素的 display CSS 属性(不会删除 DOM,只是隐藏)。
  • 适用于 需要频繁切换显示状态的场景(如:展开/折叠菜单)。

🔹 语法

vue
<p v-show="show">显示内容</p>

等价于:

vue
<p :style="{ display: show ? 'block' : 'none' }">显示内容</p>

🔹 v-show 的底层原理 ​

在 Vue 编译模板时,v-show 仅转换为 CSS 控制:

js
render() {
  return h("p", { style: { display: this.show ? "block" : "none" } }, "显示内容");
}
  • v-show 不会移除 DOM 元素,仅通过 style="display: none" 隐藏。
  • 组件的生命周期钩子仍然执行,即使不可见。

🔹 v-show 的特点 ​

✅ 切换速度快(只修改 display,不涉及 DOM 操作)。
✅ 适用于高频切换(如:标签页、弹窗)。
❌ 初次渲染仍会创建 DOM,即使 v-show="false"。


3. v-if vs v-show 的区别 ​

对比项v-ifv-show
原理直接 创建/销毁 DOM仅仅 修改 display
渲染方式首次不会渲染(false 时不渲染)首次仍然渲染(false 时 display: none)
切换性能切换开销大(涉及 DOM 操作)切换开销小(仅修改 CSS)
适用场景适用于 低频切换(如:路由页面、权限控制)适用于 高频切换(如:弹窗、Tab 切换)

🚀 选择建议

  • 需要动态创建/销毁 DOM → 使用 v-if(如:数据量大的组件)。
  • 需要频繁显示/隐藏 → 使用 v-show(如:弹出层、手风琴菜单)。

4. v-html 的原理 ​

🔹 作用

  • 将字符串解析为 HTML 并插入 DOM(可以解析标签,但有安全风险)。
  • 适用于 需要渲染 HTML 代码的场景(如:富文本编辑器、Markdown 解析)。

🔹 语法**

vue
<p v-html="htmlContent"></p>
js
data() {
  return {
    htmlContent: "<span style='color: red;'>红色文本</span>"
  };
}

🔹 渲染结果

html
<p><span style="color: red;">红色文本</span></p>

🔹 v-html 的底层原理 ​

v-html 不会编译成 Vue 组件,而是直接插入 innerHTML:

js
render() {
  return h("p", { innerHTML: this.htmlContent });
}

等价于:

js
document.querySelector("p").innerHTML = this.htmlContent;

📌 危险点:

  • 可能导致 XSS(跨站脚本攻击)
  • Vue 不会解析插入的 HTML,也不会编译 Vue 指令(如 v-bind、@click)。

🔹 XSS 安全风险

vue
<p v-html="userInput"></p>

如果 userInput 由用户输入:

js
data() {
  return {
    userInput: "<script>alert('被攻击了!')</script>"
  };
}

浏览器会执行 <script> 代码,造成 XSS 攻击!
✅ 安全做法:

  • 使用 HTML 解析库(如 sanitize-html)清理数据:
js
import sanitizeHtml from "sanitize-html";
data() {
  return {
    safeHtml: sanitizeHtml(userInput)
  };
}
  • 用 v-text 显示文本(不解析 HTML)
vue
<p v-text="htmlContent"></p>

📌 v-text 会转义 HTML,防止 XSS。


5. 总结 ​

指令作用底层原理适用场景
v-if按条件渲染(创建/销毁 DOM)createElement() / removeChild()低频切换(权限控制、路由)
v-show显示/隐藏(CSS display)style="display: none"高频切换(弹窗、Tab 切换)
v-html解析 HTML 插入 DOMinnerHTML富文本解析(⚠ 有 XSS 风险)

🚀 选择建议

  • 需要完全删除/创建元素 → 用 v-if
  • 只想控制 display,优化性能 → 用 v-show
  • 需要解析 HTML(如:文章内容) → 用 v-html(需注意 XSS)

✨ 最佳实践

  1. v-if + v-else 切换组件,避免不必要的渲染。
  2. v-show 用于高频显示/隐藏,如菜单、弹窗。
  3. 避免 v-html 直接使用用户输入的数据,防止 XSS。

🔥 Vue 的高效渲染依赖这些指令的正确使用,合理选择才能优化性能! 🚀

10. v-model 是如何实现的,语法糖实际是什么? ​

在 Vue 中,v-model 是用于双向数据绑定的语法糖,它可以让数据和 UI 之间保持同步。


1. v-model 的作用 ​

  • 简化表单输入的双向绑定
  • 监听用户输入,并自动更新数据
  • 支持 input、textarea、checkbox、radio、select 等表单控件
  • 可以用于组件,实现双向数据绑定

2. v-model 语法糖等价的写法 ​

在 Vue 2 和 Vue 3 中,v-model 的内部实现有所不同。

v-model 在原生 input 上的等价写法 ​

Vue 2 ​
vue
<input v-model="message">

等价于:

vue
<input :value="message" @input="message = $event.target.value">
  • :value="message" 让 input 的值与 message 绑定。
  • @input 事件监听用户输入,并更新 message。

Vue 3 ​
vue
<input v-model="message">

等价于:

vue
<input :value="message" @update:modelValue="message = $event">
  • Vue 3 采用 update:modelValue 事件,适用于组件。

3. v-model 的底层原理 ​

Vue 2 v-model 的实现 ​

Vue 2 使用 Object.defineProperty() 来实现数据劫持:

js
Object.defineProperty(vm, 'message', {
  get() {
    return data.message;
  },
  set(newVal) {
    data.message = newVal;
    updateDOM(); // 触发视图更新
  }
});

当 input 触发 @input 事件时:

js
document.querySelector("input").addEventListener("input", function(event) {
  vm.message = event.target.value;
});

Vue 2 通过 getter/setter 让数据和 UI 保持同步。


Vue 3 v-model 的实现 ​

Vue 3 使用 Proxy 代理整个对象:

js
const state = reactive({ message: "Hello Vue3" });

const inputElement = document.querySelector("input");
inputElement.addEventListener("input", event => {
  state.message = event.target.value; // 直接修改 Proxy
});

Vue 3 不需要 defineProperty,直接代理整个对象,提高性能。


4. v-model 在组件中的实现 ​

Vue 2 组件中的 v-model ​

在 Vue 2 中,组件的 v-model 绑定 value 和 input 事件。

子组件 ​
vue
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>

<script>
export default {
  props: ["value"]
};
</script>
父组件 ​
vue
<ChildComponent v-model="message"/>

等价于:

vue
<ChildComponent :value="message" @input="message = $event"/>

📌 Vue 2 规定 v-model 默认绑定 value,事件名是 input。


Vue 3 组件中的 v-model ​

Vue 3 改变了 v-model 的事件名称,使用 modelValue 作为默认 prop,并用 update:modelValue 作为事件。

子组件 ​
vue
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script>
export default {
  props: ["modelValue"]
};
</script>
父组件 ​
vue
<ChildComponent v-model="message"/>

等价于:

vue
<ChildComponent :modelValue="message" @update:modelValue="message = $event"/>

📌 Vue 3 的 v-model 变得更灵活,可以绑定多个值!


Vue 3 组件支持多个 v-model ​

Vue 3 允许 v-model 绑定多个不同的值:

vue
<ChildComponent v-model:title="title" v-model:content="content"/>

等价于:

vue
<ChildComponent
  :title="title"
  @update:title="title = $event"
  :content="content"
  @update:content="content = $event"
/>

📌 这样可以让组件支持多个双向绑定的值!


5. v-model vs sync ​

在 Vue 2,sync 修饰符也可以实现双向绑定:

vue
<ChildComponent :title.sync="title"/>

等价于:

vue
<ChildComponent :title="title" @update:title="title = $event"/>

📌 Vue 3 直接用 v-model 替代了 .sync,更统一!


6. 总结 ​

版本原理事件默认 Prop
Vue 2 (v-model)value + input@input="$emit('input', value)"value
Vue 3 (v-model)modelValue + update:modelValue@update:modelValue="$emit('update:modelValue', value)"modelValue
Vue 3(多 v-model)model:propName + update:propName@update:propNamepropName

🔥 结论 ​

✅ Vue 2:v-model 语法糖等价于 :value + @input
✅ Vue 3:v-model 绑定 modelValue,update:modelValue 实现双向绑定
✅ Vue 3 允许多个 v-model,比 Vue 2 更强大

💡 推荐:使用 Vue 3 方式,更灵活、更易维护!🚀

11. 对keep-alive的理解,它是如何实现的,具体缓存的是什么? ​

keep-alive 是 Vue 提供的一个内置组件,用于 缓存动态组件,避免重复创建和销毁,从而提高性能。


1. keep-alive 的作用 ​

  • 缓存动态组件,防止组件被销毁
  • 切换组件时保留状态
  • 避免重复渲染,提高性能
  • 适用于频繁切换的组件(如路由页面、Tab 切换等)

2. keep-alive 的基本使用 ​

🔹 作用:缓存组件 ​

vue
<keep-alive>
  <component :is="currentView"></component>
</keep-alive>
  • currentView 变化时,Vue 不会销毁已渲染的组件,而是 缓存并复用。

在路由中使用 keep-alive ​

vue
<keep-alive>
  <router-view></router-view>
</keep-alive>
  • 路由切换时,之前的页面不会销毁,下次切换回来时仍然保留数据。

3. keep-alive 缓存了什么? ​

keep-alive 缓存的是组件的实例(DOM + 组件状态),而不是数据。

被缓存的内容 ​

✅ 组件的 DOM 结构(不会重新创建)
✅ 组件的 data 状态(变量不会重置)
✅ 组件的 computed 计算属性
✅ 组件的 methods 方法

未缓存的内容 ​

❌ Vuex/Pinia 状态(需要手动管理)
❌ watch 监听的数据(数据变化仍然会触发)
❌ 全局变量(window 级别)


4. keep-alive 的核心原理 ​

Vue 通过 虚拟 DOM + LRU 缓存机制 来实现 keep-alive。

keep-alive 组件的核心逻辑 ​

  1. 缓存组件实例
    • 使用 cache 对象存储组件的 VNode。
  2. 复用缓存
    • 当 component 发生变化时,Vue 优先从 cache 取组件实例,而不是重新创建。
  3. 组件生命周期
    • 进入缓存时触发 activated,离开时触发 deactivated(而不是 destroyed)。

5. keep-alive 内部实现(Vue 3 源码简化版) ​

js
export default {
  setup(props, { slots }) {
    const cache = new Map(); // 缓存组件实例
    return () => {
      const vnode = slots.default();
      if (cache.has(vnode.type)) {
        return cache.get(vnode.type); // 复用缓存
      }
      cache.set(vnode.type, vnode);
      return vnode;
    };
  }
};

📌 解析

  • cache 存储已经渲染的组件实例
  • 切换时,组件不会销毁,而是复用已缓存的实例

6. keep-alive 生命周期 ​

生命周期钩子 ​

keep-alive 不会触发 created 和 destroyed,而是 activated 和 deactivated。

生命周期触发时机
created组件首次创建(仅一次)
mounted组件挂载(仅一次)
activated组件从缓存中激活
deactivated组件被缓存(但未销毁)
destroyed组件被彻底销毁(keep-alive 不触发)

示例 ​

vue
<template>
  <keep-alive>
    <MyComponent v-if="show"></MyComponent>
  </keep-alive>
  <button @click="show = !show">切换组件</button>
</template>

<script>
export default {
  data() {
    return { show: true };
  }
};
</script>
vue
<template>
  <p>我是被缓存的组件</p>
</template>

<script>
export default {
  created() {
    console.log("created"); // 只执行一次
  },
  mounted() {
    console.log("mounted"); // 只执行一次
  },
  activated() {
    console.log("activated"); // 每次显示时触发
  },
  deactivated() {
    console.log("deactivated"); // 每次隐藏时触发
  }
};
</script>

📌 控制台日志

sh
created
mounted
activated
# 切换组件
deactivated
activated

🚀 组件不会销毁,而是缓存起来,切换回来时 activated 触发!


7. keep-alive 的 include 和 exclude ​

  • include:指定哪些组件需要缓存(支持逗号分隔、数组、正则)。
  • exclude:指定哪些组件不缓存。
  • max:缓存组件的最大数量,超过后会移除最早使用的组件(LRU 机制)。

🔹 示例 ​

vue
<keep-alive include="A,B">
  <component :is="currentComponent"></component>
</keep-alive>
  • 只有 A、B 组件会被缓存,其他组件会被销毁。
vue
<keep-alive :exclude="/^Test/">
  <component :is="currentComponent"></component>
</keep-alive>
  • 所有组件 以 Test 开头的都不会缓存。
vue
<keep-alive :max="3">
  <component :is="currentComponent"></component>
</keep-alive>
  • 最多缓存 3 个组件,超过后会移除最早访问的组件(LRU 算法)。

8. keep-alive vs localStorage ​

keep-alivelocalStorage
存储位置内存(RAM)本地存储(硬盘)
数据类型组件实例(DOM + data)JSON 数据
页面刷新❌ 刷新后丢失✅ 刷新后保留
适用场景高频组件切换(如 Tab 组件)长期存储用户数据

9. keep-alive 的应用场景 ​

✅ 路由缓存

vue
<keep-alive>
  <router-view></router-view>
</keep-alive>

✅ Tab 组件

vue
<keep-alive>
  <component :is="activeTab"></component>
</keep-alive>

✅ 搜索表单缓存

  • 让用户切换页面后返回时,表单不丢失。

10. 总结 ​

keep-alive
作用缓存动态组件,避免重复渲染
缓存内容组件实例(DOM + data)
实现原理LRU 缓存机制,存储组件 VNode
生命周期activated(显示时),deactivated(隐藏时)
适用场景路由缓存、Tab 组件、搜索表单
注意事项不会缓存 Vuex/Pinia 数据,刷新页面会丢失

🚀 最佳实践

  1. keep-alive 适用于 高频切换的组件,减少不必要的渲染。
  2. 路由缓存时,配合 include 限制缓存范围,避免占用过多内存。
  3. 如果数据需要持久化,配合 localStorage 或 Vuex/Pinia 使用。

✨ keep-alive 是 Vue 中提高性能的重要方式,合理使用可以优化用户体验!🚀

12. $nextTick 原理及作用 ​

1. $nextTick 的作用 ​

$nextTick 是 Vue 提供的一个异步方法,用于在 DOM 更新完成后 执行回调函数。
它的作用是确保在 DOM 发生变化后再执行某些操作,避免获取不到最新的 DOM 结构。

为什么需要 $nextTick? ​

Vue 更新 DOM 是异步的,即:

  • 当数据变化时,Vue 不会立即更新 DOM,而是先收集所有变更,然后统一执行批量更新(Vue 内部的异步队列优化)。
  • 如果你在数据修改后立即获取 DOM,可能还是旧的值,因此需要 $nextTick 来确保 DOM 已更新。

2. $nextTick 的基本用法 ​

vue
<template>
  <div>
    <p ref="text">{{ message }}</p>
    <button @click="updateMessage">更新文本</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue"
    };
  },
  methods: {
    updateMessage() {
      this.message = "Updated Message"; // 修改数据
      console.log(this.$refs.text.innerText); // 可能仍然是 "Hello Vue"(旧值)
      
      this.$nextTick(() => {
        console.log(this.$refs.text.innerText); // "Updated Message"(新值,DOM 已更新)
      });
    }
  }
};
</script>

📌 解析

  • this.message = "Updated Message" 修改数据,但 Vue 还没有更新 DOM。
  • 直接 console.log(this.$refs.text.innerText) 可能仍然是旧值。
  • 使用 $nextTick 确保在 DOM 更新后再获取最新的值。

3. $nextTick 的底层原理 ​

Vue 内部使用微任务(Microtask)优先的方式来执行 $nextTick,确保高效更新 DOM。

Vue 内部 $nextTick 执行流程 ​

  1. 数据变更时,Vue 不会立即更新 DOM,而是将任务推入更新队列(异步更新)。
  2. 在下一次事件循环时,Vue 统一执行所有更新任务。
  3. $nextTick 让你在 DOM 更新完成后执行回调。

内部优先使用以下机制 ​

  1. Promise(Microtask)
js
Promise.resolve().then(() => callback());
  • 现代浏览器(Vue 3)优先使用。
  • 基于事件循环的微任务(Microtask),执行速度最快。
  1. MutationObserver
js
const observer = new MutationObserver(() => callback());
   observer.observe(document.body, { attributes: true });
  • 适用于部分不支持 Promise 的浏览器。
  1. setImmediate(仅适用于 IE)
js
tImmediate(() => callback());
  • IE 专用,其他浏览器不支持。
  1. setTimeout(回退方案)
js
setTimeout(() => callback(), 0);
  • 所有浏览器都支持,但速度较慢(Macrotask)。

📌 Vue 3 源码中的 $nextTick 实现(简化版)

js
let pending = false;
const callbacks = [];

function flushCallbacks() {
  pending = false;
  callbacks.forEach(cb => cb());
  callbacks.length = 0;
}

export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks);
  }
}
  • Vue 先收集所有 $nextTick 回调,然后批量执行,避免多次触发事件循环,提升性能。

4. $nextTick 的应用场景 ​

(1) 确保 DOM 更新后执行操作 ​

js
methods: {
  addItem() {
    this.items.push("New Item");
    this.$nextTick(() => {
      console.log(this.$refs.list.lastChild); // 获取新增的 DOM 元素
    });
  }
}

📌 确保 list 更新后,再获取 lastChild。


(2) 在 mounted 之后执行 DOM 依赖的代码 ​

js
mounted() {
  this.$nextTick(() => {
    console.log(this.$refs.input.focus()); // 页面加载后自动聚焦
  });
}

📌 确保 input 元素已挂载再执行 focus()。


(3) 解决 v-if 切换 DOM 时的问题 ​

js
methods: {
  showModal() {
    this.visible = true;
    this.$nextTick(() => {
      console.log(this.$refs.modal); // 确保 `modal` 已经挂载
    });
  }
}

📌 v-if 需要 nextTick 确保元素已经出现在 DOM 中。


5. $nextTick vs setTimeout ​

对比项$nextTicksetTimeout
执行时机DOM 更新后立即执行(微任务)DOM 更新后,下一次事件循环执行(宏任务)
适用于Vue DOM 相关操作一般性异步任务
执行速度更快(微任务,Promise 级别)较慢(宏任务)

📌 示例对比

js
this.message = "Hello";
this.$nextTick(() => console.log("nextTick")); // 立即执行(微任务)
setTimeout(() => console.log("setTimeout"), 0); // 下一轮事件循环执行(宏任务)

// 可能输出顺序:
// nextTick
// setTimeout

📌 $nextTick 适用于 DOM 更新后立即执行的代码。


6. 总结 ​

特性描述
作用确保 Vue DOM 更新后执行回调
执行原理Microtask(Promise)+ Event Loop 机制
适用场景操作更新后的 DOM,避免获取旧数据
优先级比 setTimeout 更快(微任务 vs 宏任务)

🚀 最佳实践

  1. 如果你需要在数据更新后操作 DOM,使用 $nextTick。
  2. 尽量避免直接修改 DOM,而是用 Vue 方式(ref、computed)管理。
  3. Vue 3 中 $nextTick 默认基于 Promise,速度更快!

✨ 总结:$nextTick 是 Vue 处理异步 DOM 更新的核心方法,掌握它可以更好地优化 Vue 项目!🚀

13. Vue中封装的数组方法有哪些,其如何实现页面更新 ​

在 Vue 2 中,由于 JavaScript 原生数组方法不会触发 Object.defineProperty() 的 setter,Vue 重写了一些数组方法,以确保数据变化能够触发视图更新。

在 Vue 3 里,使用了 Proxy 代理整个对象,不再需要手动拦截数组方法。

1. Vue 2 封装的数组方法 ​

image.png

  • 这些方法会修改原数组,Vue 2 通过劫持这些方法来触发视图更新。

2. Vue 2 如何重写数组方法 ​

Vue 2 的 defineReactive 机制 ​

Vue 2 通过 Object.defineProperty() 劫持 data,但 数组的索引不会被拦截,因此 Vue 2 采用了继承原型的方法来改写数组。

js
const arrayProto = Array.prototype; // 获取数组原型
const arrayMethods = Object.create(arrayProto); // 创建一个继承自数组原型的新对象

// 需要拦截的 7 个方法
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args); // 调用原生方法
    console.log(`Vue 监听到数组发生变化: ${method}`);
    // 触发 Vue 视图更新(这里 Vue 内部会调用 `dep.notify()`)
    return result;
  };
});

// 让响应式数组继承 Vue 代理的数组原型
const reactiveArray = [];
Object.setPrototypeOf(reactiveArray, arrayMethods);

reactiveArray.push(1); // Vue 监听到数组发生变化: push
reactiveArray.pop();   // Vue 监听到数组发生变化: pop

📌 解析

  • Vue 2 不会拦截数组索引的变化,因为 defineProperty 不能检测索引变动。
  • 解决方案:拦截 7 个修改数组的方法,使其在调用后通知 Vue 触发更新。

3. Vue 2 数组更新的触发机制 ​

触发视图更新的方式 ​

  1. 拦截修改数组的方法(push、splice 等)
  2. 手动 Vue.set()(解决索引更新的问题)
  3. 触发 dep.notify(),重新渲染视图

Vue 2 监听数组的 dep.notify() 逻辑 ​

js
const dep = {
  notify() {
    console.log("触发 Vue 视图更新");
  }
};

arrayMethods.push = function (...args) {
  const result = Array.prototype.push.apply(this, args);
  dep.notify(); // 通知 Vue 更新视图
  return result;
};

📌 如果直接修改数组索引,如 arr[0] = 10;,不会触发更新!


4. Vue 2 不能检测的数组变更 ​

Vue 2 无法检测以下两种数组修改:

  1. 直接修改索引
js
this.items[1] = "new value"; // ❌ Vue 不会检测到变化

✅ 解决方案

js
this.$set(this.items, 1, "new value"); // ✅ 触发更新
  1. 修改数组长度
js
this.items.length = 2; // ❌ Vue 不会检测到变化

✅ 解决方案

js
this.items.splice(2); // ✅ 触发更新

5. Vue 3 如何监听数组变化 ​

Vue 3 使用 Proxy 代理整个数组 ​

Vue 3 直接使用 Proxy 代理整个对象,可以监听所有操作:

js
const data = reactive({ items: [1, 2, 3] });

data.items[1] = 10;  // ✅ Vue 3 可以检测索引修改
data.items.length = 5; // ✅ Vue 3 也能检测

📌 Vue 3 不需要 Vue.set(),直接修改索引就会触发更新!

Vue 3 Proxy 实现 ​

js
const arrayHandler = {
  get(target, key) {
    console.log(`读取: ${key}`);
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    console.log(`修改: ${key} -> ${value}`);
    Reflect.set(target, key, value);
    return true;
  }
};

const arr = new Proxy([], arrayHandler);

arr.push(4);  // 读取: push | 修改: 0 -> 4
arr[1] = 10;  // 修改: 1 -> 10
arr.length = 5; // 修改: length -> 5

📌 Vue 3 Proxy 让数组的索引修改和长度变化都能被监听。


6. Vue 2 vs Vue 3 数组响应式对比 ​

Vue 2 (defineProperty)Vue 3 (Proxy)
数组方法重写 push/pop/splice 等方法直接监听所有方法
索引修改不会触发更新(需要 Vue.set())自动触发更新
数组长度变化不会触发更新自动触发更新
性能性能较低(需要拦截多个方法)性能更高(统一代理对象)

7. Vue 2 数组更新的最佳实践 ​

✅ 使用 Vue 提供的方法

js
this.items.splice(1, 1, "newValue"); // 替换索引 1 的元素
this.$set(this.items, 2, "newItem"); // 添加新元素

✅ 避免直接修改索引

js
this.items[1] = "newValue"; // ❌ Vue 2 无法检测到变化

✅ 避免修改数组长度

js
this.items.length = 3; // ❌ Vue 2 也不会检测到
this.items.splice(3); // ✅ 触发更新

✅ Vue 3 直接支持所有操作

js
this.items[1] = "newValue"; // ✅ Vue 3 自动检测
this.items.length = 3; // ✅ Vue 3 也能检测

8. 总结 ​

特性Vue 2Vue 3
数组响应式方式Object.defineProperty() + 方法劫持Proxy
支持 push()、splice()✅ 是✅ 是
支持 arr[0] = newValue❌ 否(需要 Vue.set)✅ 是
支持 arr.length = 3❌ 否(不会触发更新)✅ 是
性能较低(需要拦截方法)高(直接代理整个对象)

🚀 结论

  • Vue 2 需要手动 Vue.set() 修改索引或 splice() 变更数组长度。
  • Vue 3 直接使用 Proxy 代理数组,可以监听所有操作,性能更高!
  • Vue 3 彻底解决了 Vue 2 无法监听数组索引和长度修改的问题。

✨ 如果你的项目在 Vue 2 中遇到数组更新问题,建议迁移到 Vue 3,体验更流畅的响应式机制!🚀

14. Vue 单页应用与多页应用的区别 ​

Vue 在开发 Web 应用时,可以构建 单页应用(SPA) 或 多页应用(MPA),两者在架构、性能、SEO、用户体验等方面有所不同。


1. 什么是单页应用(SPA - Single Page Application)? ​

概念 ​

  • 单页应用(SPA) 只有一个 HTML 页面,通过 Vue Router 实现前端路由,不同的页面只是 DOM 的局部更新,不需要重新加载整个页面。
  • 典型的 Vue + Vue Router 项目就是 SPA。

工作原理 ​

  • 首次加载 时,浏览器请求 index.html 并加载 Vue 相关资源(JS、CSS)。
  • 之后的页面切换 由 Vue 动态修改 DOM,不需要重新请求整个页面,只加载必要的数据或组件。

Vue SPA 典型代码 ​

vue
<template>
  <div>
    <router-view></router-view> <!-- 通过 Vue Router 切换不同组件 -->
  </div>
</template>
js
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";

const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About }
];

const router = createRouter({ history: createWebHistory(), routes });
export default router;

2. 什么是多页应用(MPA - Multi Page Application)? ​

🔹 概念 ​

  • 多页应用(MPA) 有多个独立的 HTML 页面,每个页面是一个完整的 Vue 应用(或非 Vue 页面)。
  • 页面跳转会重新加载整个 HTML 和 JS、CSS 资源。

Vue MPA 典型代码 ​

在 vue.config.js 里配置多个 entry:

js
module.exports = {
  pages: {
    index: {
      entry: "src/main.js",
      template: "public/index.html",
      filename: "index.html"
    },
    about: {
      entry: "src/about.js",
      template: "public/about.html",
      filename: "about.html"
    }
  }
};

📌 MPA 中,每个页面都是一个独立的 Vue 实例,如:

js
// src/main.js
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

// src/about.js
import { createApp } from "vue";
import About from "./About.vue";
createApp(About).mount("#app");

📌 页面跳转时,浏览器重新加载整个 HTML 和 Vue 代码。


3. Vue SPA vs MPA 的区别 ​

对比项单页应用(SPA)多页应用(MPA)
页面结构只有一个 HTML 页面(index.html)多个 HTML 页面,每个页面独立
页面切换通过 Vue Router 进行前端路由,局部更新 DOM通过浏览器进行页面跳转,每次加载新 HTML
加载速度首屏较慢(需加载 Vue、JS 资源)首屏较快(HTML+CSS+JS 分开加载)
用户体验流畅,无刷新页面切换传统方式,页面刷新后跳转
SEO 优化默认不友好(需 SSR 或预渲染)原生支持 SEO
服务器压力较小,减少 HTTP 请求较大,每次访问都请求服务器
适合场景后台管理系统、Web App电商网站、企业官网

4. SPA 的优缺点 ​

✅ Vue SPA 的优点 ​

  1. 体验流畅:页面切换无刷新,提高用户体验。
  2. 减少服务器压力:大部分逻辑在前端处理,服务器只需提供 API。
  3. 更容易维护:Vue 组件化开发,代码结构清晰。
  4. 前后端分离:可以结合 Vue + Node.js / Java 后端 API 交互。

❌ Vue SPA 的缺点 ​

  1. 首屏加载慢:需要下载较大的 JS,可能影响用户体验。
    • 解决方案:使用 懒加载(lazy-loading)、CDN 加速。
  2. SEO 不友好:搜索引擎无法直接爬取 Vue 动态渲染的内容。
    • 解决方案:
      • SSR(服务端渲染,Nuxt.js)
      • 预渲染(prerender-spa-plugin)
  3. 浏览器兼容性:低端设备可能运行缓慢,需要优化代码。

5. MPA 的优缺点 ​

✅ Vue MPA 的优点 ​

  1. SEO 友好:每个页面都是独立的 HTML,搜索引擎可以爬取。
  2. 首屏加载快:只加载当前页面所需资源,初次访问更快。
  3. 兼容性好:适用于传统 Web 项目,支持旧版浏览器。

❌ Vue MPA 的缺点 ​

  1. 用户体验差:每次跳转都需要重新加载页面。
  2. 服务器压力大:每次请求都要向服务器获取完整 HTML 页面。
  3. 前端开发复杂:多个 HTML 页面,每个都需要单独初始化 Vue。

6. 什么时候用 Vue SPA?什么时候用 Vue MPA? ​

场景推荐方案
后台管理系统✅ SPA,操作流畅,减少服务器压力
企业官网✅ MPA,更好的 SEO 支持
电商网站✅ MPA + Vue 单个组件,SEO 友好,支持动态内容
Web App(PWA)✅ SPA,优化体验,减少请求
需要高效 SEO 的站点(如博客、新闻)✅ MPA 或 SSR(Nuxt.js)

7. Vue SPA 如何优化首屏加载? ​

  1. 使用 lazy-loading(路由懒加载)
js
const Home = () => import("@/views/Home.vue");
const About = () => import("@/views/About.vue");

📌 按需加载组件,减少首屏 JS 体积。

  1. 使用 Vue 框架优化

    • Nuxt.js(SSR 服务端渲染,提高 SEO)
    • vite + Vue 3(更快的编译速度)
  2. 使用 prerender-spa-plugin 预渲染

    • 适用于静态站点
    • Vue 先渲染 HTML,再让 SEO 爬取
  3. 使用 gzip 压缩

    • 减小 JS/CSS 体积,加快加载速度
sh
npm install compression-webpack-plugin
  1. 使用 CDN
    • 将 Vue、Element UI、Axios 放到 CDN,提高访问速度
html
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.0/dist/vue.global.js"></script>

8. 总结 ​

对比项SPA(单页应用)MPA(多页应用)
是否刷新页面❌ 无刷新,Vue Router 控制✅ 页面跳转重新加载
首屏加载速度🚀 慢(需优化)⚡ 快(HTML 直接可用)
用户体验✅ 平滑切换,像 App❌ 传统体验,页面跳转
SEO 友好性❌ 需 SSR 处理✅ 天然 SEO 友好
服务器压力✅ 低❌ 高
适用场景后台管理、Web App电商、企业官网

🚀 最佳实践

  • 后台系统 / PWA / 高交互应用 → SPA
  • 企业官网 / 需要 SEO 的网站 → MPA 或 SSR(Nuxt.js)
  • 大型电商 → MPA + Vue 组件

✨ 选择适合的架构,优化项目体验!🚀

15. Vue template 到 render 的过程 ​

在 Vue 中,template 模板最终会转换为 render 函数,然后再生成 虚拟 DOM(VNode),最后更新真实 DOM。这个过程主要包括以下步骤:

  1. 模板编译(Template -> Render 函数)
  2. 虚拟 DOM 生成(Render 函数 -> VNode)
  3. 虚拟 DOM 渲染(VNode -> 真实 DOM)
  4. 响应式更新(数据变化 -> 重新渲染 VNode -> 更新真实 DOM)

1. template 到 render 的整体流程 ​

vue
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

转换过程:

  1. 模板解析(parse):解析 <template> 为 AST(抽象语法树)。
  2. AST 转换(transform):优化 AST,转换指令等。
  3. 代码生成(generate):AST 变成 render 函数。
  4. 执行 render 函数:生成 虚拟 DOM(VNode)。
  5. VNode 通过 patch 变成真实 DOM。
  6. 数据更新时,Vue 通过 Diff 算法 只更新必要部分,优化性能。

2. Vue 3 详细流 ​

(1) template 解析成 AST(抽象语法树) ​

Vue 解析模板 <template>,转换为 AST 结构:

js
const template = `<div><p>{{ message }}</p></div>`;
const ast = parse(template);
console.log(ast);

🔹 解析结果

js
{
  type: 'Root',
  children: [
    {
      type: 'Element',
      tag: 'div',
      children: [
        {
          type: 'Element',
          tag: 'p',
          children: [
            { type: 'Interpolation', content: 'message' }
          ]
        }
      ]
    }
  ]
}

📌 作用

  • Vue 解析 HTML,构建节点树(类似 JSON 结构)。
  • 识别 {{ message }} 是插值表达式。

(2) AST 转换(指令、事件解析) ​

Vue 处理 指令(如 v-if、v-for),转换为标准 JavaScript 结构:

js
{
  type: 'Root',
  children: [
    {
      type: 'Element',
      tag: 'div',
      children: [
        {
          type: 'Element',
          tag: 'p',
          children: [
            { type: 'Expression', content: '_ctx.message' }
          ]
        }
      ]
    }
  ]
}

📌 优化

  • Vue 3 提前计算静态节点(减少运行时计算量)。
  • 指令 v-if、v-for 会转换为对应的 if 语句或 map 函数。

(3) 代码生成(AST -> render 函数) ​

Vue 3 把 AST 结构转化为 render 函数:

js
function render(_ctx) {
  return h("div", null, [
    h("p", null, _ctx.message)
  ]);
}

📌 作用

  • Vue 3 编译时,生成 h()(createVNode)函数,构造虚拟 DOM。
  • h(tag, props, children) 结构:
js
h("p", null, _ctx.message)
  • p:HTML 标签
  • null:没有属性
  • _ctx.message:组件的数据

(4) 执行 render 生成 VNode ​

js
const vnode = render.call(vm);
console.log(vnode);

🔹 VNode 结构

js
{
  type: "div",
  props: null,
  children: [
    {
      type: "p",
      props: null,
      children: "Hello Vue"
    }
  ]
}

📌 VNode 是 JavaScript 对象,不是真实 DOM。


(5) VNode 渲染成真实 DOM ​

Vue 通过 patch 进行 VNode -> 真实 DOM 渲染:

js
patch(null, vnode, container);

Vue 递归遍历 VNode,创建真实 DOM:

js
const el = document.createElement(vnode.type);
el.textContent = vnode.children;
container.appendChild(el);

📌 最终转换

html
<div>
  <p>Hello Vue</p>
</div>

3. Vue 组件数据更新流程 ​

当 message 变化时,Vue 不会重新创建整个 DOM,而是通过 Diff 算法更新 VNode:

  1. 监听数据变化
  2. 生成新的 VNode
  3. Diff 算法比对新旧 VNode
  4. 最小化 DOM 更新

4. render 手写示例 ​

Vue 允许使用 render 代替 template:

vue
<script>
import { h } from "vue";

export default {
  render() {
    return h("div", null, [
      h("p", null, this.message)
    ]);
  },
  data() {
    return { message: "Hello Vue" };
  }
};
</script>

📌 Vue h() 函数

  • h(tag, props, children)
  • 等价于 createVNode("p", null, this.message)

5. Vue template 到 render 的完整流程 ​

阶段作用示例
1. 解析(Parse)template 转 AST 结构{ tag: "p", children: "{{ message }}" }
2. 转换(Transform)处理指令 & 变量解析{ tag: "p", children: "_ctx.message" }
3. 生成(Generate)生成 render 函数h("p", null, _ctx.message)
4. 渲染(Render)render() 生成 VNode{ type: "p", children: "Hello Vue" }
5. 挂载(Patch)VNode -> 真实 DOM<p>Hello Vue</p>

6. Vue 2 vs Vue 3 编译差异 ​

对比项Vue 2(Virtual DOM)Vue 3(Block Tree 优化)
编译目标VNode 虚拟 DOMBlock Tree 结构
静态提升❌ 不能单独优化静态节点✅ 静态节点缓存
Diff 算法全量 Diff(性能较低)优化 Diff(减少不必要更新)

📌 Vue 3 优化

  • 静态提升:不变的节点 只编译一次,避免重复计算。
  • Block Tree:VNode 结构更优化,减少 Diff 计算量。

7. 总结 ​

Vue template 到 render 流程 ​

  1. 解析 template,转换为 AST(抽象语法树)。
  2. 优化 AST,处理指令、事件、插值表达式。
  3. 生成 render 函数,用于创建 虚拟 DOM(VNode)。
  4. 执行 render,将 VNode 转换为真实 DOM 并渲染。
  5. 数据变化时,Vue 进行 Diff 比较,最小化更新 DOM。

Vue 3 主要优化 ​

✅ 静态提升,减少重复渲染
✅ Block Tree 结构,提高 Diff 性能
✅ 更高效的 render 生成方式

✨ Vue template 只是编写方式,最终都会转成 render 执行!掌握这个流程可以帮助优化 Vue 应用!🚀

16. 描述下Vue自定义指令(Directives) ​

1. 什么是 Vue 自定义指令? ​

Vue 内置了一些指令(如 v-if、v-for、v-model 等),但在某些场景下,我们需要自定义指令来直接操作 DOM,比如:

  • 手动聚焦输入框
  • 监听元素滚动
  • 实现拖拽功能
  • 节流、权限控制

Vue 提供 directive API 定义自定义指令,可以在 组件或全局注册。


2. 自定义指令的基本语法 ​

(1) 局部自定义指令 ​

在 Vue 组件中,使用 directives 选项定义指令:

vue
<template>
  <input v-focus />
</template>

<script>
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    }
  }
};
</script>

📌 解释

  • v-focus 绑定到 <input>,当元素 mounted 时,执行 el.focus() 让输入框自动获取焦点。

(2) 全局自定义指令 ​

使用 app.directive() 在 Vue 3 中注册全局指令:

js
import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

// 注册全局指令 v-focus
app.directive("focus", {
  mounted(el) {
    el.focus();
  }
});

app.mount("#app");

📌 所有组件都可以使用 v-focus。


3. Vue 3 自定义指令的生命周期 ​

钩子函数Vue 2Vue 3作用
bind✅❌指令第一次绑定到元素(Vue 3 移除)
inserted✅❌绑定的元素插入到 DOM(Vue 3 移除)
update✅❌组件更新时调用(Vue 3 移除)
componentUpdated✅❌组件更新后调用(Vue 3 移除)
unbind✅❌指令解绑时触发(Vue 3 移除)
created❌✅Vue 3 新增,指令创建时调用
beforeMount❌✅Vue 3 新增,指令绑定前调用
mounted✅✅元素插入 DOM 时调用
beforeUpdate❌✅绑定元素更新前调用
updated✅✅元素更新后调用
beforeUnmount❌✅绑定元素卸载前调用
unmounted✅✅元素被移除时调用

4. Vue 3 自定义指令完整示例 ​

vue
<template>
  <input v-focus v-color="'red'" />
</template>

<script>
export default {
  directives: {
    // 自动聚焦
    focus: {
      mounted(el) {
        el.focus();
      }
    },

    // 颜色指令
    color: {
      mounted(el, binding) {
        el.style.color = binding.value;
      },
      updated(el, binding) {
        el.style.color = binding.value;
      }
    }
  }
};
</script>

📌 说明

  • v-focus:输入框自动聚焦。
  • v-color="'red'":绑定 color 指令,接收参数 binding.value 作为颜色值。

5. binding 绑定对象 ​

指令的 binding 对象提供传递值、参数和修饰符的信息:

vue
<template>
  <p v-demo:font-size.big.red="'blue'">自定义指令</p>
</template>

<script>
export default {
  directives: {
    demo: {
      mounted(el, binding) {
        console.log(binding);
      }
    }
  }
};
</script>

📌 binding 结构

js
{
  value: "blue",        // 传递的值('blue')
  oldValue: undefined,  // 旧值(仅在 updated 钩子可用)
  arg: "font-size",     // 传递的参数(:font-size)
  modifiers: {          // 修饰符对象
    big: true,
    red: true
  }
}

6. 高级案例 ​

(1) 指令实现 v-throttle(节流) ​

vue
<template>
  <button v-throttle="handleClick">点击节流</button>
</template>

<script>
export default {
  directives: {
    throttle: {
      mounted(el, binding) {
        let timer = null;
        el.addEventListener("click", () => {
          if (!timer) {
            timer = setTimeout(() => {
              binding.value();
              timer = null;
            }, 1000);
          }
        });
      }
    }
  },
  methods: {
    handleClick() {
      console.log("节流触发", new Date().toLocaleTimeString());
    }
  }
};
</script>

📌 每隔 1 秒才触发一次 handleClick,防止按钮被疯狂点击。


(2) 指令实现 v-draggable(拖拽) ​

vue
<template>
  <div v-draggable class="box">拖拽我</div>
</template>

<script>
export default {
  directives: {
    draggable: {
      mounted(el) {
        el.style.position = "absolute";
        el.onmousedown = (e) => {
          let startX = e.clientX - el.offsetLeft;
          let startY = e.clientY - el.offsetTop;
          
          document.onmousemove = (event) => {
            el.style.left = event.clientX - startX + "px";
            el.style.top = event.clientY - startY + "px";
          };
          
          document.onmouseup = () => {
            document.onmousemove = null;
            document.onmouseup = null;
          };
        };
      }
    }
  }
};
</script>

<style>
.box {
  width: 100px;
  height: 100px;
  background: lightblue;
  text-align: center;
  line-height: 100px;
  cursor: move;
}
</style>

📌 实现了一个拖拽功能,拖动 div 时改变 left/top 位置。


7. Vue 3 vs Vue 2 自定义指令区别 ​

对比项Vue 2Vue 3
全局注册Vue.directive()app.directive()
生命周期bind、inserted、update、unbindcreated、mounted、updated、unmounted
binding 对象binding.value 传递值支持更多钩子(beforeMount、beforeUpdate)

8. 什么时候使用自定义指令? ​

✅ 需要直接操作 DOM(如 focus、color)。
✅ 全局通用行为(如 v-throttle、v-draggable)。
✅ 复杂的 DOM 操作,避免写在 mounted 中。

🚀 Vue 3 推荐优先使用 Composition API 代替指令:

js
const vFocus = {
  mounted(el) {
    el.focus();
  }
};
vue
<template>
  <input v-focus />
</template>
<script setup>
import { vFocus } from "@/directives";
</script>

9. 总结 ​

特性Vue 2Vue 3
局部指令directives: {}directives: {}
全局指令Vue.directive()app.directive()
生命周期bind、insertedmounted、updated
适用场景操作 DOM、滚动监听、权限控制推荐用 Composition API 代替

🚀 Vue 3 中,优先使用 Composition API 和 ref,仅在需要直接操作 DOM 时使用 directive!

17. 子组件可以直接改变父组件的数据吗? ​

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。

Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

18. Vue是如何收集依赖的? ​

特性Vue 2(Object.defineProperty)Vue 3(Proxy)
监听方式逐个 defineProperty 监听Proxy 代理整个对象
依赖收集Dep 依赖管理WeakMap 依赖管理
新增属性不能动态监听,需要 Vue.set()自动监听
数组监听拦截 push/pop/splice 等方法直接监听
性能依赖收集较慢性能更高
  • Vue 通过 get() 触发依赖收集,在 set() 触发更新。
  • Vue 2 通过 Object.defineProperty 拦截属性,Vue 3 通过 Proxy 监听整个对象。
  • Vue 3 依赖收集更高效: ✅ 支持动态属性监听(无需 Vue.set())
    ✅ 支持数组索引变更监听
    ✅ 整体性能更高,避免 Vue 2 的递归遍历

🚀 Vue 3 采用 Proxy 彻底优化了依赖收集,提高了性能,推荐升级!

19. 对 React 和 Vue 的理解,它们的异同 ​

Vue 和 React 是目前最流行的前端框架,它们都用于构建 组件化的 UI,但设计理念、状态管理、渲染机制等方面有所不同。下面从多个维度对比它们的异同点。


1. React 和 Vue 的相同点 ​

共同点解释
组件化开发采用 组件化思想,复用 UI 逻辑
虚拟 DOM通过 Virtual DOM 进行高效渲染
数据驱动视图UI 依赖状态数据,数据变化自动更新 UI
单向数据流都支持 单向数据绑定,但 Vue 额外支持双向绑定
支持 JSX/TemplateReact 使用 JSX,Vue 也支持 JSX(但默认用模板)
支持 Composition API / HooksReact 有 Hooks,Vue 3 有 Composition API
支持服务端渲染(SSR)React 可用 Next.js,Vue 可用 Nuxt.js

2. React vs Vue 的不同点 ​

(1) 设计理念 ​

对比项ReactVue
核心理念函数式编程(Functional UI)渐进式框架(Progressive)
开发模式只关注 UI 层,其他功能交给外部库提供完整生态(路由、状态管理)
应用场景适合 大型前端项目,可自由组合库适合 中小型项目,可渐进集成

📌 Vue 更易上手,React 更灵活但自由度更高。


(2) 组件写法 ​

Vue 采用 单文件组件(SFC),而 React 主要使用 JSX 语法。

Vue ​
vue
<template>
  <div @click="handleClick">{{ message }}</div>
</template>

<script setup>
import { ref } from "vue";
const message = ref("Hello Vue");
const handleClick = () => (message.value = "Clicked!");
</script>

<style scoped>
div { 
  color: blue; 
}
</style>
React ​
jsx
import { useState } from "react";

function App() {
  const [message, setMessage] = useState("Hello React");

  return <div onClick={() => setMessage("Clicked!")}>{message}</div>;
}

export default App;

📌 Vue 更结构化(SFC),React 使用 JSX 直观。


(3) 模板语法 vs JSX ​

对比项Vue(模板语法)React(JSX)
写法类似 HTML,语法直观纯 JavaScript,灵活可扩展
事件绑定@click="handleClick"onClick={handleClick}
样式绑定:style="{ color: 'red' }"style={{ color: 'red' }}
条件渲染v-if="isShow"{ isShow ? <div>Show</div> : null }
列表渲染v-for="item in list"{ list.map(item => <div>{item}</div>) }
📌 Vue 适合 HTML+JS 分离风格,React 更接近 JavaScript 逻辑编写。

(4) 状态管理 ​

对比项ReactVue
组件内状态useState()ref() / reactive()
全局状态管理Redux / Zustand / RecoilVuex(Vue 2)/ Pinia(Vue 3)
Vue ​
js
import { ref } from "vue";
const count = ref(0);
React ​
js
import { useState } from "react";
const [count, setCount] = useState(0);

📌 Vue 3 的 ref() 作用类似 React 的 useState()。


(5) 事件处理 ​

Vue 和 React 事件绑定方式不同:

Vue ​
vue
<button @click="handleClick">Click</button>
React ​
jsx
<button onClick={handleClick}>Click</button>

📌 Vue 采用 @,React 采用 on(JSX 事件遵循驼峰命名)。


(6) 路由 ​

Vue 使用 Vue Router,React 使用 React Router。

Vue Router ​
js
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";

const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About }
];

const router = createRouter({ history: createWebHistory(), routes });
export default router;
React Router ​
jsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./Home";
import About from "./About";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}
export default App;

📌 Vue 路由直接绑定组件,React 需要 element={<Component />}。


(7) 响应式 ​

对比项VueReact
数据变化直接修改 ref.valueuseState 需要 setState
依赖追踪reactive() 代理整个对象useEffect() 手动监听依赖
渲染优化只更新依赖的组件React.memo / useCallback 手动优化
Vue 响应式 ​
js
import { reactive } from "vue";
const state = reactive({ count: 0 });
state.count++; // 直接修改会触发 UI 更新
React 响应式 ​
js
import { useState } from "react";
const [count, setCount] = useState(0);
setCount(count + 1); // 需要手动调用 setState 触发更新

📌 Vue 依赖自动追踪,React 需要 useEffect() 监听依赖。


(8) 生态系统 ​

对比项VueReact
状态管理Vuex / PiniaRedux / Recoil / Zustand
路由Vue RouterReact Router
SSRNuxt.jsNext.js
服务端渲染✅ Nuxt.js 内置✅ Next.js 更强
跨端开发Weex / uni-appReact Native

📌 React 在跨端支持(React Native)方面更成熟,Vue 生态更完整(Pinia + Vue Router + Nuxt.js)。


(9). 结论 ​

对比项VueReact
上手难度✅ 简单,模板语法直观⚡ 需要掌握 JSX / Hooks
灵活性🔥 约定大于配置✅ 完全自由组合
性能优化✅ 自动依赖追踪⚡ 需要手动优化
适用场景后台管理、Web 应用大型项目、移动端、跨端开发

🚀 Vue 适合中小型项目,React 适合大型项目和跨平台开发!

20. Vue的优点 ​

  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
  • 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  • 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
  • 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
  • 虚拟DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
  • 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。

21. assets和static的区别 ​

相同点:

  • assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点

**不相同点:

  • 压缩 assets 中存放的静态资源文件在项目打包时,也就是运行 npm run build 时会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html 一同上传至服务器。
  • 不压缩 static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。

建议:

  • 将项目中 template需要的样式文件js文件等都可以放置在 assets 中,走打包这一流程。减少体积。
  • 而项目中引入的第三方的资源文件如iconfoont.css 等文件可以放置在 static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。

22. delete和Vue.delete删除数组的区别 ​

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete 直接删除了数组 改变了数组的键值。

23. vue如何监听对象或者数组某个属性的变化 ​

监听方式适用场景Vue 3 语法
监听对象的某个属性user.name 变化watch(() => user.value.name, callback)
监听整个对象监听 user 任何属性变化watch(user, callback, { deep: true })
监听数组整体变化监听 items 的增删watch(items, callback, { deep: true })
监听数组的某个索引items[1] 变化watch(() => items.value[1], callback)
监听数组对象的某个属性items[i].name 变化watch(() => items.value.map(item => item.name), callback)
自动收集依赖监听多个属性watchEffect(() => console.log(user.value.name))

🚀 推荐

  • 监听对象某个属性:用 watch(() => obj.value.xxx)
  • 监听数组某个元素:用 watch(() => arr.value[i])
  • 监听数组对象的某个属性:用 watch(() => arr.value.map(item => item.xxx))
  • 监听多个依赖:用 watchEffect()

✨ Vue 3 watchEffect() 自动收集依赖,watch() 更适合精准监听!🚀

24. 对SSR的理解 ​

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR的优势:

  • 更好的SEO
  • 首屏加载速度更快

SSR的缺点:

  • 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
  • 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
  • 更多的服务端负载。

25. Vue的性能优化有哪些 ​

(1)编码阶段

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
  • v-if和v-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 在更多的情况下,使用v-if替代v-show
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

(2)SEO优化

  • 预渲染
  • 服务端渲染SSR

(3)打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

(4)用户体验

  • 骨架屏
  • PWA
  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

26. template和jsx的有什么分别? ​

对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader编译.vue文件,内部依赖的vue-template-compiler模块,在 webpack 构建过程中,将template预编译成 render 函数。与 react 类似,在添加了jsx的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写render函数。

所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。

27. vue初始化页面闪动问题 ​

使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。

首先:在css里加上以下代码:

vue
[v-cloak] {    display: none;}

如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"

评论区

欢迎留言、补充或勘误。

xiaoba.blog