前端梳理体系从常问问题去完善-框架篇(Vue2&Vue3)

前言

对于Vue2,Vue3,我项目上用得不是很多,用得最多得还是React,也不知为啥,我是自学的Vue2出来得,然后出来找到得工作是React得,直到现在都是React,虽然中间也维护Vue得项目,写写Vue得项目还是可以得,不过嘛,框架原理大致都是相通得,所以整理得不像React那么细致。

对于知识体系,需要学一遍,之所以,以问题得方式去梳理就跟我们刷题一样,多刷才能记住嘛,通过提问得方式,去记住他,查缺补漏,这就是我为什么分享了四篇体系概念篇得原因,对于计算机网咯,还有移动端,小程序,这种虽然整理有,但不是很细致。有机会在分享吧。

Vue2&Vue3

Vue3 的defineProps,defineEmits,defineExpose

在 Vue3 的 <script setup> 语法糖中,definePropsdefineEmitsdefineExpose 是三个核心的编译时宏 (compiler macros),用于处理组件的 props 接收、事件触发和内部成员暴露,无需手动导入即可使用。它们是 Vue3 为简化组件逻辑、提升开发效率设计的语法糖,仅在 <script setup> 中生效。

1. defineProps:声明组件接收的 Props

用于在子组件中声明可以从父组件接收的属性(props),类似 Vue2 中的 props 选项或 Vue3 非 setup 语法中的 props 配置。它的作用是定义 props 的类型、默认值和校验规则,同时让 TypeScript 能够正确推断类型。

基本用法:

vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <div>父组件传递的消息:{{ msg }}</div>
  <div>用户年龄:{{ age }}</div>
</template>

<script setup>
// 方式1:仅声明类型(TypeScript)
const props = defineProps<{
  msg: string;
  age?: number; // 可选属性
}>();

// 方式2:声明类型 + 默认值(需用 withDefaults 辅助函数)
const props = withDefaults(defineProps<{
  msg: string;
  age?: number;
}>(), {
  age: 18, // age 的默认值
});

// 方式3:JavaScript 中使用(对象形式声明)
const props = defineProps({
  msg: {
    type: String,
    required: true, // 必传
  },
  age: {
    type: Number,
    default: 18, // 默认值
  },
});

// 使用 props
console.log(props.msg); // 访问父组件传递的 msg
</script>

核心特点:

  • 无需导入defineProps 是编译时宏,Vue 会自动处理,无需 import
  • 只读性 :返回的 props 对象是只读的(响应式代理),不能直接修改(如需修改,应通过 emit 通知父组件)。
  • 类型支持 :在 TypeScript 中可通过泛型直接声明类型,配合 withDefaults 可设置默认值;JavaScript 中通过对象形式声明(类似 Vue2 的 props 选项)。

2. defineEmits:声明组件触发的事件

用于在子组件中声明可以触发的事件(类似 Vue2 中的 $emitemits 选项),作用是定义事件的类型、参数 ,让父组件可以通过 @事件名 监听,同时提供类型校验和 TypeScript 类型推断。

基本用法:

vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">点击触发事件</button>
  <input @input="handleInput" placeholder="输入内容" />
</template>

<script setup>
// 方式1:TypeScript 中声明事件类型(泛型)
const emit = defineEmits<{
  // 事件名: (参数1类型, 参数2类型) => 返回值(通常为 void)
  'change': (value: string) => void;
  'update-age': (newAge: number) => void;
}>();

// 方式2:JavaScript 中声明事件(数组或对象形式)
const emit = defineEmits(['change', 'update-age']);
// 或带校验的对象形式
const emit = defineEmits({
  'change': (value) => typeof value === 'string', // 校验参数是否为字符串
  'update-age': (newAge) => newAge > 0, // 校验年龄是否为正数
});

// 触发事件(传递参数)
const handleClick = () => {
  emit('update-age', 20); // 触发 update-age 事件,传递参数 20
};

const handleInput = (e) => {
  emit('change', e.target.value); // 触发 change 事件,传递输入值
};
</script>

父组件监听事件:

vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    @change="onChange" 
    @update-age="onUpdateAge" 
  />
</template>

<script setup>
const onChange = (value) => {
  console.log('子组件输入:', value);
};

const onUpdateAge = (newAge) => {
  console.log('新年龄:', newAge);
};
</script>

核心特点:

  • 事件声明:明确组件可触发的事件,增强代码可读性和可维护性。
  • 参数校验 :JavaScript 中可通过函数对事件参数进行校验(返回 true 表示校验通过)。
  • 类型安全:TypeScript 中可通过泛型定义事件参数类型,触发时会自动校验参数类型。

3. defineExpose:暴露组件内部成员

<script setup> 中,组件的内部方法、属性默认是私有 的(父组件无法通过 ref 访问)。defineExpose 用于将组件内部的方法或属性暴露出去 ,让父组件可以通过 ref 获取子组件实例并访问这些成员。

基本用法:

vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <div>内部计数:{{ count }}</div>
</template>

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

// 内部状态和方法
const count = ref(0);
const increment = () => {
  count.value++;
};
const reset = () => {
  count.value = 0;
};

// 暴露给父组件的成员(仅暴露的内容可被父组件访问)
defineExpose({
  count,
  increment,
  reset,
});
</script>

父组件通过 ref 访问子组件暴露的成员:

vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

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

// 获取子组件实例的 ref
const childRef = ref(null);

const callChildMethod = () => {
  // 访问子组件暴露的 count
  console.log('子组件当前计数:', childRef.value.count.value);
  
  // 调用子组件暴露的 increment 方法
  childRef.value.increment();
  
  // 调用子组件暴露的 reset 方法
  // childRef.value.reset();
};
</script>

核心特点:

  • 选择性暴露 :仅通过 defineExpose 声明的成员会被暴露,未声明的仍为私有。
  • 避免过度耦合 :谨慎使用,过度暴露会导致组件间耦合度升高,优先通过 propsemit 通信。
  • TypeScript 支持 :可通过 defineComponent 或接口定义子组件暴露的类型,实现类型推断。

总结

  • defineProps:处理父组件到子组件的数据输入,定义接收的 props 类型和默认值。
  • defineEmits:处理子组件到父组件的事件输出,声明可触发的事件及参数。
  • defineExpose:将子组件的内部成员暴露给父组件,用于特殊场景下的组件交互(谨慎使用)。

这三个 API 是 Vue3 <script setup> 语法中组件通信的核心工具,配合使用可实现清晰、类型安全的组件交互逻辑。

Vue3 watch和 watchEffect的区别

在 Vue3 中,watchwatchEffect 都是用于监听响应式数据变化并执行副作用的 API,但它们的设计理念和使用场景有显著区别。核心差异体现在监听方式、执行时机、依赖追踪等方面,具体如下:

1. 监听目标:明确指定 vs 自动追踪

  • watch:需要明确指定监听的数据源
    watch 必须手动指定要监听的响应式数据(如 refreactive 的属性、计算属性等),只有当这些指定的数据源变化时,才会触发回调。

    vue 复制代码
    <script setup>
    import { ref, watch } from 'vue';
    const count = ref(0);
    const name = ref('vue');
    
    // 明确监听 count 的变化
    watch(count, (newVal, oldVal) => {
      console.log(`count 从 ${oldVal} 变到 ${newVal}`);
    });
    
    // 监听多个数据源(数组形式)
    watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
      console.log('count 或 name 变化了');
    });
    
    // 监听 reactive 对象的某个属性(需用 getter 函数)
    const user = reactive({ age: 18 });
    watch(() => user.age, (newAge) => {
      console.log(`年龄变为 ${newAge}`);
    });
    </script>
  • watchEffect:自动追踪函数内部的响应式数据
    watchEffect 不需要指定监听目标,它会自动追踪回调函数内部用到的所有响应式数据,当这些数据变化时,自动触发回调。

    vue 复制代码
    <script setup>
    import { ref, watchEffect } from 'vue';
    const count = ref(0);
    const name = ref('vue');
    
    // 自动追踪 count 和 name(函数内用到的响应式数据)
    watchEffect(() => {
      console.log(`count: ${count.value}, name: ${name.value}`);
    });
    // 初始化时执行一次(输出 "count: 0, name: vue")
    // 当 count 或 name 变化时,会重新执行
    </script>

2. 执行时机:懒执行 vs 立即执行

  • watch:默认懒执行
    watch 的回调只会在监听的数据源变化时执行 ,初始化时(页面加载时)不会执行。

    (可通过 immediate: true 配置改为立即执行)

    javascript 复制代码
    watch(count, () => {
      console.log('count 变化了'); // 初始时不执行,只有 count 变了才执行
    }, { immediate: true }); // 加上 immediate 后,初始化时会执行一次
  • watchEffect:默认立即执行
    watchEffect 的回调在初始化时会立即执行一次 (用于收集初始依赖),之后当依赖变化时再次执行。

    (这是因为它需要通过首次执行来 "发现" 内部用到的响应式数据)

3. 回调参数:关注新旧值 vs 仅关注副作用

  • watch 的回调:接收新旧值
    watch 的回调函数有三个参数:newVal(新值)、oldVal(旧值)、onCleanup(清理函数),适合需要对比数据变化前后状态的场景。

    javascript 复制代码
    watch(count, (newVal, oldVal, onCleanup) => {
      // 对比新旧值
      if (newVal > oldVal) {
        console.log('count 增加了');
      }
      // 清理副作用(如防抖、取消请求)
      onCleanup(() => {
        // 下次回调执行前或组件卸载时触发
      });
    });
  • watchEffect 的回调:仅接收清理函数
    watchEffect 的回调只接收一个 onCleanup 参数,不提供新旧值(因为它自动追踪依赖,无法精确知道哪个数据变化),适合只需要执行副作用(如发请求、更新 DOM)的场景。

    javascript 复制代码
    watchEffect((onCleanup) => {
      // 发送请求(依赖 count)
      const timer = setTimeout(() => {
        console.log(`count 为 ${count.value}`);
      }, 1000);
      // 清理副作用(避免多次请求)
      onCleanup(() => clearTimeout(timer));
    });

4. 依赖追踪精度:精确控制 vs 自动全量

  • watch:可精确监听部分依赖

    对于 reactive 对象,watch 可以通过 getter 函数只监听某个具体属性,避免不必要的触发。

    javascript 复制代码
    const user = reactive({ name: 'vue', age: 18 });
    // 只监听 age 的变化(name 变化不会触发)
    watch(() => user.age, () => {
      console.log('只有年龄变化时触发');
    });
  • watchEffect:自动追踪所有依赖

    只要回调函数中用到的响应式数据发生变化,无论是否是 "关键数据",都会触发回调。如果函数内用到多个数据,任何一个变化都会执行。

    javascript 复制代码
    const user = reactive({ name: 'vue', age: 18 });
    watchEffect(() => {
      // 用到了 name 和 age,任何一个变化都会触发
      console.log(`${user.name} 的年龄是 ${user.age}`);
    });

5. 使用场景

场景 推荐 API 原因
需要知道数据变化的 "新旧值"(如表单验证、比较变化) watch 回调提供 newValoldVal,方便对比
只需要在依赖变化时执行副作用(如发请求、更新 DOM) watchEffect 自动追踪依赖,代码更简洁
需精确控制监听的数据源(避免无关变化触发) watch 可手动指定监听目标,减少不必要的执行
初始化时需要立即执行一次副作用(如初始加载数据) watchEffect 默认立即执行,无需额外配置

总结

  • watch 是 "精确监听":需手动指定目标,懒执行,提供新旧值,适合需要精确控制和对比变化的场景。
  • watchEffect 是 "自动追踪":无需指定目标,立即执行,不提供新旧值,适合简单的副作用场景,代码更简洁。

两者都可以通过返回的函数停止监听(const stop = watch(...)const stop = watchEffect(...),调用 stop() 即可)。

从new Vue到页面实例使用经历那些步骤

  1. 创建项目

使用 Vue CLI 创建项目:

bash 复制代码
# 安装 Vue CLI(如果未安装)
npm install -g @vue/cli

# 创建 Vue 2 项目
vue create my-vue2-app --default
cd my-vue2-app
npm install
  1. 项目结构概览

Vue 2 项目的核心结构:

plaintext 复制代码
src/
  ├── App.vue           # 根组件
  ├── main.js           # 入口文件
  ├── components/       # 组件目录
  ├── router/           # 路由配置(可选)
  └── store/            # Vuex 状态管理(可选)
  1. 定义组件(.vue 文件)

使用单文件组件(SFC)结构,包含 <template><script><style>

vue 复制代码
<!-- src/components/HelloWorld.vue -->
<template>
  <div class="hello">
    <h1>{{ message }}</h1>
    <button @click="increment">计数: {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      message: 'Hello Vue 2!',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<style scoped>
.hello {
  padding: 20px;
}
</style>
  1. 在 App.vue 中使用组件
vue 复制代码
<!-- src/App.vue -->
<template>
  <div id="app">
    <HelloWorld />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue';

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  margin-top: 60px;
}
</style>
  1. 入口文件配置(main.js)

创建 Vue 实例并挂载到 DOM:

javascript 复制代码
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router'; // 路由(如果使用)

// 关闭生产提示
Vue.config.productionTip = false;

// 创建 Vue 实例
new Vue({
  router,           // 注入路由(如果使用)
  render: h => h(App),
}).$mount('#app');
  1. HTML 模板(public/index.html)
html 复制代码
<!DOCTYPE ht>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 2 App</title>
</head>
<body>
  <div id="app"></div>
  <!-- Vue CLI 会自动注入 JS/CSS -->
</body>
</html>
  1. 响应式原理

Vue 2 使用 Object.defineProperty()

javascript 复制代码
export default {
  data() {
    return {
      count: 0,
      user: {
        name: 'John',
        age: 30
      }
    }
  },
  created() {
    // 修改数据会触发视图更新
    this.count++;
    this.user.age = 31;
  }
}
  1. 生命周期钩子

Vue 2 组件的生命周期方法:

javascript 复制代码
export default {
  beforeCreate() {
    console.log('组件实例初始化后,数据观测和 event/watcher 事件配置前');
  },
  created() {
    console.log('实例已经创建完成之后被调用');
  },
  beforeMount() {
    console.log('挂载开始之前被调用');
  },
  mounted() {
    console.log('挂载完成后被调用(DOM 已渲染)');
  },
  beforeUpdate() {
    console.log('数据更新前被调用');
  },
  updated() {
    console.log('数据更新后被调用');
  },
  beforeDestroy() {
    console.log('实例销毁之前被调用');
  },
  destroyed() {
    console.log('实例销毁之后被调用');
  }
}
  1. 运行项目
bash 复制代码
npm run serve   # 开发环境
npm run build   # 生产环境构建

关键概念(Vue 2 特有)

  1. Options API :通过 datamethodscomputed 等选项组织代码。

  2. 响应式限制

    • 无法检测对象属性的添加或删除
    • 数组通过特定方法触发更新(如 push()splice()
  3. 混合(Mixins):代码复用机制(Vue 3 推荐 Composition API)

  4. 过滤器(Filters):格式化文本(Vue 3 中移除,推荐使用计算属性)

完整示例(待办应用)

vue 复制代码
<!-- src/components/TodoList.vue -->
<template>
  <div class="todo-list">
    <h1>{{ title }}</h1>
    
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办">
    <button @click="addTodo">添加</button>
    
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        <input type="checkbox" v-model="todo.done">
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="removeTodo(index)">删除</button>
      </li>
    </ul>
    
    <p>已完成: {{ completedCount }} / {{ todos.length }}</p>
  </div>
</template>

<script>
export default {
  name: 'TodoList',
  data() {
    return {
      title: 'Vue 2 待办列表',
      newTodo: '',
      todos: [
        { text: '学习 Vue 2', done: false },
        { text: '掌握 Options API', done: false }
      ]
    }
  },
  computed: {
    completedCount() {
      return this.todos.filter(todo => todo.done).length;
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.todos.push({ text: this.newTodo, done: false });
        this.newTodo = '';
      }
    },
    removeTodo(index) {
      this.todos.splice(index, 1);
    }
  }
}
</script>

<style scoped>
.done {
  text-decoration: line-through;
  color: #888;
}
</style>

总结

Vue 2 框架下的开发流程:

  1. 项目初始化:使用 Vue CLI 创建项目。
  2. 组件化开发:使用单文件组件(SFC)结构。
  3. Options API :通过 datamethodscomputed 等选项组织代码。
  4. 实例创建与挂载 :通过 new Vue() 创建实例并挂载。
  5. 响应式更新 :基于 Object.defineProperty() 实现数据响应式。

Vue 2 是一个成熟的框架,适合现有项目维护或对学习曲线有要求的团队。对于新项目,推荐考虑 Vue 3 及其 Composition API。

Vue核心实现原理

Vue.js 的核心实现原理围绕响应式系统虚拟 DOM组件化生命周期展开。以下是其核心机制的详细解析:

一、响应式系统(Reactivity System)

Vue 通过 Object.defineProperty() (Vue 2.x)或 Proxy(Vue 3.x)实现数据劫持,当数据变化时自动更新 DOM。

核心流程:

  1. 数据劫持
    • Vue 初始化时遍历 data 对象,将所有属性转换为 getter/setter
    • 每个属性关联一个 Dep(依赖收集器)。
  2. 依赖收集
    • 组件渲染时,访问数据触发 getter,将当前渲染函数(Watcher)添加到 Dep 的依赖列表。
  3. 通知更新
    • 数据变更时触发 setter,Dep 通知所有依赖的 Watcher 更新。

简化代码示例(Vue 2.x 原理):

javascript 复制代码
class Vue {
  constructor(options) {
    this.data = options.data;
    this.observe(this.data);
    // 创建渲染 Watcher
    new Watcher(this, this.render);
  }

  observe(obj) {
    if (!obj || typeof obj !== 'object') return;
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) dep.depend(); // 收集依赖
        return value;
      },
      set(newValue) {
        if (value === newValue) return;
        value = newValue;
        dep.notify(); // 通知更新
      }
    });
    this.observe(value); // 递归处理嵌套对象
  }
}

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (Dep.target) this.subscribers.add(Dep.target);
  }

  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

class Watcher {
  constructor(vm, updateFn) {
    this.vm = vm;
    this.updateFn = updateFn;
    Dep.target = this;
    this.updateFn(); // 触发依赖收集
    Dep.target = null;
  }

  update() {
    this.updateFn(); // 更新视图
  }
}

二、虚拟 DOM(Virtual DOM)

Vue 使用 JavaScript 对象(VNode)表示真实 DOM 结构,通过差异化算法高效更新 DOM。

核心流程:

  1. VNode 生成
    • 模板编译或手写 render 函数生成 VNode 树。
  2. Diff 算法
    • 新旧 VNode 对比,找出最小变更集。
    • 采用 双指针 + key 追踪 优化性能。
  3. Patch 操作
    • 根据差异更新真实 DOM。

简化 Diff 算法示例:

javascript 复制代码
function patch(oldVnode, newVnode) {
  // 1. 节点类型不同,直接替换
  if (oldVnode.tag !== newVnode.tag) {
    oldVnode.el.parentNode.replaceChild(createEl(newVnode), oldVnode.el);
    return;
  }

  // 2. 节点相同,更新属性
  const el = newVnode.el = oldVnode.el;
  updateProperties(el, oldVnode.props, newVnode.props);

  // 3. 处理子节点
  if (typeof newVnode.text === 'string') {
    // 文本节点
    el.textContent = newVnode.text;
  } else {
    // 递归处理子节点
    updateChildren(el, oldVnode.children, newVnode.children);
  }
}

function updateChildren(parentEl, oldChildren, newChildren) {
  // 双指针 + key 优化的 Diff 算法
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  let oldStartVnode = oldChildren[0];
  let oldEndVnode = oldChildren[oldEndIdx];
  let newStartVnode = newChildren[0];
  let newEndVnode = newChildren[newEndIdx];

  // 循环比较新旧子节点
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIdx];
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 头头比较
      patch(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 尾尾比较
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      // 头尾比较(移动节点)
      patch(oldStartVnode, newEndVnode);
      parentEl.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      // 尾头比较(移动节点)
      patch(oldEndVnode, newStartVnode);
      parentEl.insertBefore(oldEndVnode.el, oldStartVnode.el);
      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else {
      // 使用 key 进行映射查找(优化)
      const idxInOld = findIndexInOld(newStartVnode, oldChildren, oldStartIdx, oldEndIdx);
      if (idxInOld > -1) {
        const vnodeToMove = oldChildren[idxInOld];
        patch(vnodeToMove, newStartVnode);
        parentEl.insertBefore(vnodeToMove.el, oldStartVnode.el);
        oldChildren[idxInOld] = null;
      } else {
        // 新增节点
        parentEl.insertBefore(createEl(newStartVnode), oldStartVnode.el);
      }
      newStartVnode = newChildren[++newStartIdx];
    }
  }

  // 处理剩余节点
  if (newStartIdx <= newEndIdx) {
    const refEl = newEndIdx + 1 < newChildren.length ? newChildren[newEndIdx + 1].el : null;
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentEl.insertBefore(createEl(newChildren[i]), refEl);
    }
  }

  if (oldStartIdx <= oldEndIdx) {
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldChildren[i]) {
        parentEl.removeChild(oldChildren[i].el);
      }
    }
  }
}

三、组件化与生命周期

Vue 组件是独立的实例,拥有自己的生命周期和作用域。

核心机制:

  1. 组件实例化
    • 每个组件都是 Vue 构造函数的实例。
    • 组件间通过 propsevents 通信。
  2. 生命周期钩子
    • 关键阶段:beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed
  3. 异步渲染队列
    • 多次数据变更会合并为一次 DOM 更新,通过 nextTick 访问更新后的 DOM。

生命周期简化流程图:

plaintext 复制代码
创建实例 → 初始化数据 → 编译模板 → 挂载 DOM → 数据变更 → 虚拟 DOM  diff → 更新 DOM → 销毁实例

四、模板编译

Vue 将模板字符串编译为 render 函数,生成 VNode。

编译流程:

  1. 解析(Parse)
    • 将模板字符串转换为 AST(抽象语法树)。
  2. 优化(Optimize)
    • 标记静态节点,避免重复 diff。
  3. 生成(Generate)
    • 将 AST 转换为 render 函数代码。

示例:

html 复制代码
<!-- 模板 -->
<div id="app">
  <p>{{ message }}</p>
</div>

编译后的 render 函数:

javascript 复制代码
function render() {
  return createVNode('div', { id: 'app' }, [
    createVNode('p', null, [this.message])
  ]);
}

五、Vue 3.x 核心改进

  1. Proxy 响应式系统
    • 解决 Vue 2.x 对象新增属性、数组索引修改等限制。
  2. 组合式 API(Composition API)
    • 通过 setup() 函数实现逻辑复用和代码组织。
  3. Tree-Shaking 支持
    • 按需打包,减小生产包体积。
  4. 性能优化
    • 更高效的 Diff 算法(PatchFlag)、静态提升(Static Hoisting)等。

六、常见面试问题解答

  1. Vue 响应式原理是什么?
    • Vue 通过 Object.defineProperty()Proxy 劫持数据的 getter/setter,结合依赖收集和发布订阅模式实现。
  2. Vue 如何检测数组变化?
    • Vue 2.x 通过重写数组原型方法(如 push, pop)实现监听,Vue 3.x 直接使用 Proxy
  3. 虚拟 DOM 的优缺点?
    • 优点:减少 DOM 操作次数、跨平台支持(SSR、移动端);
    • 缺点:首次渲染效率较低、复杂场景 diff 算法可能耗时。
  4. Vue 生命周期钩子的作用?
    • 例如 created 用于数据初始化,mounted 用于 DOM 操作,beforeDestroy 用于清理资源。

总结

Vue 的核心优势在于响应式系统 的优雅设计、虚拟 DOM 的高效更新和组件化的开发模式。理解这些原理有助于写出更高效、可维护的 Vue 应用,同时也能更好地应对性能优化和疑难问题排查。

Vue2 和 Vue3的区别

Vue 2 和 Vue 3 是 Vue.js 框架的两个主要版本,Vue 3 在保持与 Vue 2 兼容性的同时引入了多项重大改进。以下是它们的核心区别及升级建议:

一、架构与性能

  1. 响应式系统
  • Vue 2

    • 基于

      复制代码
      Object.defineProperty()

      实现,存在以下限制:

      • 无法检测对象属性的添加或删除。
      • 数组通过劫持原型方法实现响应式,部分操作(如直接通过索引修改)需手动处理。
  • Vue 3

    • 使用 ES6 Proxy 重构响应式系统,解决了上述问题:
      • 可以检测对象属性的添加 / 删除。
      • 对数组操作的响应式支持更全面。
      • 性能提升约 2 倍(更少的依赖追踪开销)。
  1. 编译优化
  • Vue 3

    • 引入

      Block Tree

      Patch Flag

      等编译时优化:

      • 静态内容不再参与虚拟 DOM 比较,提升渲染效率。
      • 动态绑定标记更精确,仅更新变化的部分。
  1. 体积优化
  • Vue 3
    • 通过 Tree-Shaking 移除未使用的代码,核心体积减少约 41%。

二、API 设计

  1. 组合式 API(Composition API)
  • Vue 3

    • 新增

      Composition API

      (基于

      复制代码
      setup()

      函数或

      复制代码
      <script setup>

      ):

      vue

      vue 复制代码
      <script setup>
      import { ref, onMounted } from 'vue';
      
      const count = ref(0);
      
      const increment = () => {
        count.value++;
      };
      
      onMounted(() => {
        console.log('Component mounted');
      });
      </script>
    • 解决了 Vue 2 选项式 API(Options API)的以下问题:

      • 代码复用困难 :逻辑分散在 datamethodscomputed 等选项中。
      • 大型组件难以维护:相关逻辑被拆分在不同选项,导致 "碎片化"。
  1. 选项式 API 的变化
  • Vue 3
    • 保留大部分选项式 API,但有以下调整:
      • data 必须是函数。
      • 生命周期钩子改名(如 beforeDestroybeforeUnmount)。
      • 新增 setup() 选项作为组件的入口点。

三、组件与模块

  1. 多根组件(Fragment)
  • Vue 3

    • 组件可以有多个根节点

      vue 复制代码
      <template>
        <header>...</header>
        <main>...</main>
        <footer>...</footer>
      </template>
  1. Teleport(传送门)
  • Vue 3

    • 允许将组件渲染到 DOM 中的其他位置:

      vue 复制代码
      <teleport to="body">
        <div class="modal">...</div>
      </teleport>
  1. Suspense(异步组件)
  • Vue 3

    • 内置对异步组件的支持:

      vue 复制代码
      <Suspense>
        <template #default>
          <AsyncComponent />
        </template>
        <template #fallback>
          <LoadingSpinner />
        </template>
      </Suspense>

四、TypeScript 支持

  • Vue 2

    • 需要通过 vue-class-componentvue-property-decorator 等插件支持 TypeScript,集成不够自然。
  • Vue 3

    • 从底层设计支持 TypeScript,组合式 API 提供了更友好的类型推导:

      typescript 复制代码
      import { ref, computed } from 'vue';
      
      const count = ref(0); // 类型自动推断为 Ref<number>
      const double = computed(() => count.value * 2); // Ref<number>

五、生态与兼容性

  1. Vue Router
  • Vue 3

    • 需要使用 Vue Router 4.x,支持组合式 API:

      typescript 复制代码
      import { useRoute, useRouter } from 'vue-router';
      
      const route = useRoute();
      const router = useRouter();
  1. Vuex
  • Vue 3
    • 推荐使用 Pinia 替代 Vuex,提供更简洁的 API 和更好的 TypeScript 支持。
  1. 插件与库
  • 部分 Vue 2 插件需要更新后才能兼容 Vue 3(如 vuex-persistedstate)。

六、升级建议

  1. 新项目
  • 优先选择 Vue 3 + TypeScript + 组合式 API,充分利用新特性和性能优化。
  1. 现有项目升级
  • 渐进式迁移 :使用 Vue 3 的 兼容构建版本(compatible build),允许在 Vue 3 中使用部分 Vue 2 特性。
  • Vue CLI → Vite:考虑迁移到 Vite 构建工具,提升开发体验。

总结

特性 Vue 2 Vue 3
响应式原理 Object.defineProperty() Proxy
API 风格 选项式 API 组合式 API + 选项式 API
TypeScript 支持 有限支持,需额外插件 原生支持
多根组件 不支持 支持
异步组件 需要第三方库 内置 Suspense 组件
性能 良好 显著提升
体积 较大 更小(Tree-Shaking)

Vue 3 在保持与 Vue 2 兼容性的同时,解决了长期存在的痛点(如 TypeScript 支持、大型组件维护),并提供了更现代的 API 设计和性能优化。对于新项目和有能力升级的现有项目,Vue 3 是更好的选择。

Vue怎么实现双向数据绑定,一些原理性的问题

Vue 的双向数据绑定(Two-Way Data Binding)是其核心特性之一,本质是数据变化时自动更新视图,视图变化时自动同步数据 ,形成 "数据 ↔ 视图" 的闭环。其实现依赖于 响应式系统模板编译事件监听 三大核心机制,Vue 2 和 Vue 3 在底层实现上有差异,但整体思路一致。

一、核心原理:双向绑定的 "双向" 拆解

双向绑定的本质是 "两个单向绑定的结合":

  1. 数据 → 视图:数据变化时,自动更新视图(依赖响应式系统和视图更新机制)。
  2. 视图 → 数据:视图变化时(如用户输入),自动同步数据(依赖事件监听)。

二、数据 → 视图:数据驱动视图的原理

当数据发生变化时,Vue 能自动更新视图,核心依赖 响应式系统依赖收集机制

1. 响应式系统:数据劫持(监听数据变化)

Vue 通过 "劫持" 数据的读取和修改操作,实现对数据变化的感知。

  • Vue 2 实现:Object.defineProperty()

    Vue 2 对数据(data 中的对象)的每个属性通过 Object.defineProperty() 重写 gettersetter

    • getter:当属性被读取时触发,用于收集依赖(记录 "谁在使用这个数据")。
    • setter:当属性被修改时触发,用于通知依赖更新(告诉 "使用这个数据的地方" 重新渲染)。

    示例简化代码

    javascript 复制代码
    function defineReactive(obj, key, value) {
      // 递归处理嵌套对象
      observe(value); 
      Object.defineProperty(obj, key, {
        get() {
          // 收集依赖(如 Watcher)
          Dep.target && dep.addSub(Dep.target); 
          return value;
        },
        set(newValue) {
          if (newValue !== value) {
            value = newValue;
            observe(newValue); // 新值若为对象,继续劫持
            // 通知所有依赖更新
            dep.notify(); 
          }
        }
      });
    }
  • Vue 3 实现:Proxy

    Vue 3 改用 ES6 的 Proxy 代理整个对象(而非单个属性),解决了 Vue 2 的局限性:

    • 支持检测对象属性的新增 / 删除Object.defineProperty 无法做到)。
    • 支持数组的索引修改 (如 arr[0] = 1)和长度变化 (如 arr.length = 0)。

    示例简化代码:

    javascript 复制代码
    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key) {
          const value = Reflect.get(target, key);
          // 收集依赖(如 Effect)
          track(target, key); 
          return isObject(value) ? reactive(value) : value; // 递归代理
        },
        set(target, key, newValue) {
          Reflect.set(target, key, newValue);
          // 通知依赖更新
          trigger(target, key); 
        }
      });
    }

2. 依赖收集:记录 "谁在使用数据"

数据变化时,需要知道哪些地方(如视图、计算属性)依赖了该数据,才能精准更新。这一过程称为 "依赖收集"。

  • Vue 2 中的角色
    • Dep:每个响应式属性对应一个 Dep 实例,用于管理依赖(存储使用该属性的 Watcher)。
    • Watcher:"依赖" 的具体载体(如组件渲染、计算属性、$watch 回调)。当数据变化时,Dep 会通知所有关联的 Watcher 执行更新。
  • Vue 3 中的角色
    • Effect 替代 Watchertrack 函数收集依赖(记录当前活跃的 Effect),trigger 函数触发所有关联的 Effect 执行。

3. 视图更新:从数据变化到 DOM 渲染

当数据变化触发 setter(Vue 2)或 Proxy.set(Vue 3)后,会通过以下流程更新视图:

  1. 通知所有依赖(Watcher/Effect)"数据变了"。
  2. 依赖触发更新函数(如组件的渲染函数),生成新的虚拟 DOM(VNode)。
  3. 通过虚拟 DOM 的 diff 算法 对比新旧 VNode,计算出最小更新范围。
  4. 将差异应用到真实 DOM,完成视图更新。

三、视图 → 数据:视图驱动数据的原理

当用户操作视图(如输入框输入、按钮点击)时,Vue 通过事件监听同步更新数据,实现视图到数据的反向绑定。

v-model(双向绑定的语法糖)为例:

复制代码
  v-model

在输入框(如

复制代码
  <input>

)上会被编译为:

预览

html 复制代码
<!-- 模板 -->
<input v-model="message">

<!-- 编译后等价于:单向数据绑定 + 事件监听 -->
<input :value="message" @input="message = $event.target.value">
  • :value="message":数据 → 视图的单向绑定(数据变化时更新输入框值)。
  • @input="message = $event.target.value":视图 → 数据的同步(用户输入时,通过 input 事件更新 message 数据)。

四、Vue 2 与 Vue 3 双向绑定的核心差异

机制 Vue 2 Vue 3
数据劫持方式 Object.defineProperty() Proxy
依赖收集载体 Watcher 实例 Effect 函数
缺陷 无法监听对象新增 / 删除属性、数组索引修改 无上述缺陷,原生支持所有数据操作
性能 依赖追踪开销较大 更高效的依赖追踪,性能提升约 2 倍

总结

Vue 双向数据绑定的核心逻辑是:

  1. 通过 数据劫持Object.definePropertyProxy)感知数据变化。
  2. 通过 依赖收集WatcherEffect)记录数据与视图的关联。
  3. 数据变化时,通过 虚拟 DOM diff 更新视图(数据 → 视图)。
  4. 视图变化时,通过 事件监听 同步数据(视图 → 数据)。

这一机制让开发者无需手动操作 DOM,只需关注数据逻辑,大幅提升开发效率。

虚拟DOM和真实DOM的区别

虚拟 DOM(Virtual DOM)和真实 DOM(Real DOM)是前端开发中的两个重要概念,它们的主要区别如下:

  1. 定义与结构
  • 真实 DOM
    是由浏览器提供的 API,是文档的树形结构表示,每个节点都是一个对象,直接与浏览器渲染引擎交互。操作真实 DOM 的代价很高,因为每次修改都会触发浏览器的重排(reflow)和重绘(repaint)。
  • 虚拟 DOM
    是真实 DOM 的抽象表示,通常用 JavaScript 对象或轻量级数据结构(如 React 中的element)来模拟 DOM 树。它是真实 DOM 的 "虚拟映射",不直接参与渲染。
  1. 性能差异
  • 真实 DOM
    直接操作会导致频繁的重排和重绘,性能开销大,尤其在复杂应用中容易出现卡顿。
  • 虚拟 DOM
    通过批量计算差异(Diff 算法),只将必要的变更一次性应用到真实 DOM 上,减少渲染次数,提高性能。
  1. 操作方式
  • 真实 DOM

    操作直接影响页面,例如:

    javascript 复制代码
    document.getElementById('app').innerHTML = '<div>Hello World</div>';
  • 虚拟 DOM

    通过状态变化触发重新渲染,框架内部计算差异后更新真实 DOM,例如 React 中的 JSX:

    jsx 复制代码
    function App() {
      return <div>Hello World</div>;
    }
  1. 应用场景
  • 真实 DOM
    适合简单交互或直接操作特定元素的场景(如动画、临时 UI 更新)。
  • 虚拟 DOM
    适合复杂 UI 和频繁更新的应用(如单页应用),通过减少 DOM 操作提升效率。
  1. 典型框架
  • 真实 DOM
    原生 JavaScript、jQuery 等直接操作 DOM 的库。
  • 虚拟 DOM
    React、Vue.js(2.x 及以后)、Angular 等现代框架。

总结

虚拟 DOM 通过抽象和解耦 DOM 操作,减少了直接操作真实 DOM 的性能损耗,尤其在大型应用中优势明显。但它并非银弹,在简单场景下可能带来额外的复杂度。

前端Key有什么作用

在前端开发中,key 是一个特殊的属性(常见于 React、Vue 等框架),主要用于优化列表渲染性能和确保 DOM 元素的正确识别。其核心作用如下:

1. 标识列表项的唯一性

当渲染列表(如 v-formap 生成的元素)时,key 用于标识每个列表项的唯一身份

框架通过 key 判断元素是否为新创建、已存在或需要删除,从而避免对整个列表重新渲染。

示例(Vue)

vue 复制代码
<ul>
  <li v-for="item in list" :key="item.id">
    {{ item.name }}
  </li>
</ul>

示例(React)

jsx 复制代码
<ul>
  {list.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
  • 这里 item.id 作为 key,确保每个列表项有唯一标识。

2. 优化 DOM 渲染性能

框架(如 React、Vue)采用虚拟 DOM 机制,通过对比新旧虚拟 DOM 树来更新真实 DOM。
key 的存在让框架能快速定位:

  • 相同 key:元素可能只是内容变化,无需重新创建 DOM 节点(仅更新内容)。
  • 不同 key:元素为新节点,需要创建;或旧节点已被移除,需要删除。

反例:不使用 key 或使用索引作为 key 的问题

若用数组索引(index)作为 key,当列表发生增删、排序 时,key 可能会与实际元素错位,导致:

  • 不必要的 DOM 节点销毁与重建(性能浪费)。
  • 状态错乱(如表单输入值、组件状态与 DOM 不匹配)。

示例

原列表 [A, B, C] 用索引 0,1,2 作为 key,若在头部插入 D,新列表变为 [D, A, B, C],此时 Akey0 变为 1,框架会误判 A 是新元素并重建,导致性能损耗。

3. 确保组件状态的正确复用

对于列表中的组件,key 决定组件实例是否复用:

  • key 不变,组件实例会被复用(保留内部状态,如 datastate)。
  • key 变化,组件会被销毁并重新创建(重置内部状态)。
vue 复制代码
<!-- Vue 中通过改变 key 重置组件 -->
<my-component :key="activeTab" />

4. 避免渲染错误

在某些场景下,缺少 key 可能导致渲染异常:

  • 列项包含表单元素(如 input)时,可能出现输入值与显示内容不匹配。
  • 动画或过渡效果错乱(框架无法正确识别元素的插入 / 删除状态)。

使用 key 的注意事项

  1. 唯一性key 在当前列表中必须唯一,不能重复(否则会报错或导致渲染异常)。
  2. 稳定性key 应尽量稳定(如用后端返回的 id),避免使用易变的值(如索引 index 或随机数)。
  3. 作用域key 的唯一性仅针对当前列表,不同列表的 key 可以重复。

总结

key 的核心作用是帮助框架高效识别列表项的身份 ,从而优化 DOM 更新性能、确保组件状态正确复用。

开发中应优先使用数据自身的唯一标识 (如 id)作为 key,避免滥用索引或随机值。

怎么判断一个空对象,空数组

  • 判断空对象 :优先使用 Object.keys(obj).length === 0,若需包含不可枚举属性,使用 Reflect.ownKeys()
  • 判断空数组 :直接使用 arr.length === 0,若需严格校验元素,结合 Array.every()
  • 通用场景 :使用 lodash.isEmpty() 或自定义函数结合多种方法。
  1. 后端接口返回的数据为空时,前端怎么判断

前端判断后端返回的 "空数据",核心是:

  1. 明确 "空" 的具体形式(结合接口文档);
  2. 针对性判断 (区分 null、空对象、空数组等);
  3. 避免误判 (如不把 0false 当作空值)。

合理的空值判断能提升代码健壮性,避免因数据异常导致的页面崩溃或逻辑错误。

  1. Vue2 set 是什么,用来做什么

    在 Vue2 中,Vue.set(或其别名 this.$set)是一个全局 API,用于解决 Vue2 响应式系统的深层响应式限制 。它的主要作用是向响应式对象中添加一个新属性,并确保这个新属性同样具备响应式能力。

    为什么需要 Vue.set?

    Vue2 的响应式系统基于 Object.defineProperty() 实现,它在初始化时会递归遍历 data 对象的所有属性,将其转换为 getter/setter。但这种方式存在两个限制:

    1. 无法检测对象属性的添加或删除
      当你直接给响应式对象添加一个新属性时(如 this.obj.newProp = 'value'),Vue2 无法自动将这个新属性转换为响应式的,因此不会触发视图更新。
    2. 无法检测数组索引的直接修改
      当你通过索引直接修改数组元素时(如 this.arr[0] = 'new value'),Vue2 也无法捕获到这个变化。

Vue.set 的用法

Vue.set 接受三个参数:

javascript 复制代码
Vue.set(target, propertyName/index, value)
  • target:要添加属性的响应式对象或数组。
  • propertyName/index:属性名或数组索引。
  • value:新属性的值。

示例 1:给对象添加响应式属性

javascript 复制代码
export default {
data() {
 return {
   user: {
     name: 'John',
   }
 }
},
methods: {
 addAge() {
   // 错误:非响应式,视图不会更新
   // this.user.age = 30;
   
   // 正确:使用 Vue.set 确保响应式
   Vue.set(this.user, 'age', 30);
   
   // 或使用 this.$set 别名(组件内)
   this.$set(this.user, 'age', 30);
 }
}
}

示例 2:更新数组元素

javascript 复制代码
export default {
data() {
 return {
   items: ['a', 'b', 'c']
 }
},
methods: {
 updateFirstItem() {
   // 错误:非响应式,视图不会更新
   // this.items[0] = 'A';
   
   // 正确:使用 Vue.set 更新数组元素
   Vue.set(this.items, 0, 'A');
   
   // 或使用数组变异方法(Vue2 已对这些方法做了响应式处理)
   this.items.splice(0, 1, 'A');
 }
}
}

替代方案

除了 Vue.set,还有一些场景可以使用其他方式实现响应式更新:

  1. 替换整个对象

    如果需要添加多个属性,可以用新对象替换原对象:

    javascript 复制代码
    // 替换整个 user 对象,确保所有新属性都是响应式的
    this.user = {
      ...this.user,  // 保留原有属性
      age: 30,
      gender: 'male'
    }
  2. 使用数组变异方法

    Vue2 对数组的某些方法(如 pushsplice)做了响应式处理:

    javascript 复制代码
    // 添加元素(响应式)
    this.items.push('new item');
    
    // 删除元素(响应式)
    this.items.splice(1, 1);

注意事项

复制代码
  1. **Vue3 中的变化**
     Vue3 使用 Proxy 实现响应式系统,基本解决了上述限制,因此不再需要 `Vue.set`。但为了兼容 Vue2 代码,Vue3 仍保留了这个 API,但多数场景已不再需要。
  2. **性能考虑**
     频繁使用 `Vue.set` 可能影响性能,尤其是在大型对象或数组中。尽量在初始化时就声明所有需要的属性。
  3. **深层对象**
     如果需要给深层嵌套的对象添加属性,可能需要先确保父级对象已经是响应式的。

总结

Vue.set 是 Vue2 中用于解决响应式系统限制的重要工具,主要用于:

  • 向响应式对象添加新属性。
  • 更新数组元素(通过索引)。

在 Vue2 项目中,当你发现直接赋值无法触发视图更新时,优先考虑使用 Vue.set 或其替代方案。

Vuex讲一下

Vuex 是专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。以下是关于 Vuex 的核心概念、工作流程及应用场景的详细介绍:

一、核心概念

  1. State
  • 单一数据源 :应用的所有状态集中存储在一个 store 中。
  • 响应式:Vuex 的 state 变化会自动触发视图更新。
  1. Getter
  • 计算属性 :类似于 Vue 组件中的 computed,用于获取 state 的派生数据。
  • 缓存机制:依赖的 state 不变时,多次调用不会重复计算。
  1. Mutation
  • 唯一修改途径:修改 state 必须通过提交 mutation。
  • 同步操作:确保状态变化可追踪和调试。
  1. Action
  • 异步操作:处理异步逻辑(如 API 请求),完成后提交 mutation。
  • 分发(dispatch) :通过 store.dispatch() 触发。
  1. Module
  • 模块化:将 store 分割成多个模块,每个模块有自己的 state、mutation、action 等。

二、工作流程

plaintext 复制代码
组件触发 Action(异步操作) → Action 提交 Mutation → Mutation 修改 State → State 变化触发视图更新

关键流程说明

  1. 组件中触发 Action

    javascript 复制代码
    this.$store.dispatch('fetchUserInfo');
  2. Action 处理异步逻辑

    javascript 复制代码
    actions: {
      fetchUserInfo({ commit }) {
        api.getUser().then(data => {
          commit('SET_USER', data); // 提交 mutation
        });
      }
    }
  3. Mutation 修改 State

    javascript 复制代码
    mutations: {
      SET_USER(state, user) {
        state.user = user; // 直接修改 state
      }
    }
  4. 组件获取 State

    javascript 复制代码
    computed: {
      user() {
        return this.$store.state.user;
      }
    }

三、应用场景

  1. 多组件共享状态
    • 如用户信息、主题设置、购物车数据等。
  2. 复杂数据流管理
    • 多级组件嵌套通信(替代事件总线或 prop 层层传递)。
  3. 状态持久化
    • 结合插件(如 vuex-persistedstate)将 state 存储到本地存储或会话存储。
  4. 服务端渲染(SSR)
    • 在 Vue SSR 中预取数据并同步到客户端。

四、代码示例

1. 基础 Store 结构

javascript 复制代码
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
    user: null
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    },
    fetchUser({ commit }) {
      return api.getUser().then(user => {
        commit('setUser', user);
        return user;
      });
    }
  }
});

2. 在组件中使用

vue 复制代码
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="incrementAsync">Async +</button>
  </div>
</template>

<script>
export default {
  computed: {
    // 获取 state
    count() {
      return this.$store.state.count;
    },
    // 获取 getter
    doubleCount() {
      return this.$store.getters.doubleCount;
    }
  },
  methods: {
    // 提交 mutation
    increment() {
      this.$store.commit('increment');
    },
    // 分发 action
    incrementAsync() {
      this.$store.dispatch('incrementAsync');
    }
  }
};
</script>

五、高级特性

  1. 模块化

    javascript 复制代码
    // store/modules/cart.js
    export default {
      namespaced: true, // 启用命名空间
      state: { items: [] },
      mutations: { /* ... */ },
      actions: { /* ... */ },
      getters: { /* ... */ }
    };
    
    // 在主 store 中引入模块
    new Vuex.Store({
      modules: {
        cart: cartModule
      }
    });
  2. 插件

    • 日志插件:记录 mutation 变化。
    • 持久化插件:自动同步 state 到本地存储。
  3. 严格模式

    • 开发环境中强制所有 state 变更通过 mutation,防止意外修改。

六、Vuex vs Pinia

Vuex 是 Vue 2 的主流状态管理库,而 Pinia 是 Vue 3 推荐的新方案,相比 Vuex 有以下优势:

  • 更简洁的 API(无需编写 mutations)。
  • 更好的 TypeScript 支持。
  • 更小的体积和更灵活的模块化。

Vuex 仍适用于现有 Vue 2 项目,而新项目建议优先考虑 Pinia。

总结

Vuex 通过集中式状态管理解决了复杂应用中的数据共享和数据流问题,遵循单向数据流原则,使状态变化可预测且易于调试。在中大型 Vue 应用中,合理使用 Vuex 能显著提升代码可维护性和开发效率。

Vue 如何做部分响应式得监听比如obj={a:1,c:,d:{}}只监听a,c,d不监听

在 Vue 中实现部分属性的响应式监听,可以根据 Vue 版本的不同采用不同的方案。核心思路是只让需要监听的属性(a、c)成为响应式,而不需要监听的属性(d)保持非响应式。

Vue 2 实现方式

Vue 2 基于 Object.defineProperty 实现响应式,只有初始化时在 data 中声明的属性才会被转为响应式。可以利用这一特性,让不需要监听的属性(d)在初始化后再添加,从而避免被响应式处理。

javascript 复制代码
export default {
  data() {
    return {
      obj: {
        a: 1,
        c: 2 // 只在初始化时声明需要监听的属性
      }
    };
  },
  created() {
    // 初始化后手动添加不需要监听的属性d
    // 注意:不要使用 this.$set,否则会被转为响应式
    this.obj.d = {};
  },
  watch: {
    // 监听a的变化
    'obj.a'(newVal) {
      console.log('a变化了:', newVal);
    },
    // 监听c的变化
    'obj.c'(newVal) {
      console.log('c变化了:', newVal);
    }
  },
  methods: {
    updateProps() {
      this.obj.a = 10; // 会触发watch监听
      this.obj.c = 20; // 会触发watch监听
      this.obj.d = { x: 1 }; // 不会触发任何响应式更新
    }
  }
};

原理

Vue 2 只会对 data 初始化时存在的属性(a、c)进行响应式处理,后续直接添加的属性(d)不会被拦截,因此修改 d 不会触发组件更新或 watch 监听。

Vue 3 实现方式

Vue 3 基于 Proxy 实现响应式,默认会对对象的所有属性(包括新增属性)进行监听。需要通过 shallowReactive 或手动分离属性来实现部分响应式。

方案 1:使用 shallowReactive(浅响应式)

shallowReactive 只会使对象的第一层属性成为响应式,嵌套属性(如 d 内部的属性)不会被响应式处理。但如果只是不想监听 d 本身,可以结合手动赋值:

javascript 复制代码
import { reactive, shallowReactive } from 'vue';

export default {
  setup() {
    // 用shallowReactive创建浅响应式对象(只监听第一层属性)
    const obj = shallowReactive({
      a: 1,
      c: 2
    });

    // 手动添加不需要监听的属性d(不会被响应式处理)
    obj.d = {};

    // 监听a和c的变化
    watch(
      () => obj.a,
      (newVal) => console.log('a变化了:', newVal)
    );
    watch(
      () => obj.c,
      (newVal) => console.log('c变化了:', newVal)
    );

    const updateProps = () => {
      obj.a = 10; // 会触发监听
      obj.c = 20; // 会触发监听
      obj.d = { x: 1 }; // 不会触发监听
    };

    return { obj, updateProps };
  }
};

方案 2:分离响应式与非响应式属性

将需要监听的属性(a、c)放在响应式对象中,不需要监听的属性(d)放在普通对象中:

javascript 复制代码
import { reactive, watch } from 'vue';

export default {
  setup() {
    // 响应式对象:只包含需要监听的a和c
    const reactiveObj = reactive({
      a: 1,
      c: 2
    });

    // 普通对象:不需要监听的d
    const normalObj = {
      d: {}
    };

    // 合并为一个对象使用(可选)
    const obj = { ...reactiveObj, ...normalObj };

    // 监听a和c
    watch(
      () => reactiveObj.a,
      (newVal) => console.log('a变化了:', newVal)
    );

    const updateProps = () => {
      reactiveObj.a = 10; // 触发监听
      normalObj.d = { x: 1 }; // 不触发监听
    };

    return { obj, updateProps };
  }
};

原理

Vue 3 的 reactive 会对所有属性(包括新增)进行响应式处理,而 shallowReactive 只处理第一层属性。通过分离属性或使用浅响应式,可以避免不需要的属性(d)被监听。

总结

  • Vue 2:利用初始化时未声明的属性不会被响应式处理的特性,后续手动添加非监听属性。
  • Vue 3 :使用 shallowReactive 限制响应式层级,或通过分离对象将非监听属性放在普通对象中。

两种方式都能实现只监听 ac,而忽略 d 的变化。

Vue2 provide/inject为什么有意设计不是响应式,而Vue3又是

Vue2 中 provide/inject 设计为非响应式,而 Vue3 使其支持响应式,本质上是由框架设计理念、响应式系统实现差异 以及使用场景定位共同决定的,具体原因如下:

一、Vue2 中 provide/inject 非响应式的设计考量

  1. 核心定位:依赖注入而非状态管理
    Vue2 中 provide/inject 的设计初衷是解决深层嵌套组件间的配置传递问题 (如主题、国际化配置等静态 / 低频变动的数据),而非用于动态状态共享。
    例如:一个组件库的底层组件可能需要获取顶层组件的配置(如是否禁用动画),这类配置通常在初始化后不会频繁变化,因此无需响应式。
  2. 避免滥用导致的数据流混乱
    Vue2 强调 "单向数据流"(父→子通过 props,子→父通过事件),而 provide/inject 本质上是 "跨层级透传",如果支持响应式,可能会导致:
    • 开发者过度依赖它实现状态共享,替代 Vuex 或 props,使数据流向变得隐蔽(难以追踪谁修改了数据)。
    • 响应式依赖链变长,增加调试复杂度(比如深层组件修改注入的数据,难以定位来源)。
  3. 响应式系统的技术限制
    Vue2 的响应式基于 Object.defineProperty,对对象 / 数组的拦截存在天然限制(如无法监听新增属性、数组索引修改等)。若要让 provide/inject 支持响应式,需要手动将数据包装为 Vue.observable(Vue2 中让对象响应式的方法),但这会增加使用成本,且不符合其 "轻量配置传递" 的定位。

二、Vue3 中 provide/inject 支持响应式的原因

  1. 响应式系统的底层升级
    Vue3 改用 Proxy 实现响应式,能原生支持对对象、数组的完整拦截(包括新增属性、删除属性等),且响应式数据的包装(ref/reactive)更轻量、直观。
    这使得 provide/inject 可以自然地传递响应式数据 ------ 只需将 refreactive 对象通过 provide 传递,inject 后即可直接触发响应式更新,无需额外处理。
  2. 使用场景的扩展:配合 Composition API
    Vue3 引入的 Composition API 鼓励将逻辑按功能拆分(而非按选项),这使得跨组件共享 "带响应式的业务逻辑" 成为常见需求。
    例如:在组件树中共享用户登录状态(user = ref(null)),需要在多个层级的组件中实时响应状态变化。此时 provide/inject 作为轻量级跨层级通信方案,必须支持响应式才能满足需求。
  3. 更灵活的 "响应式控制权"
    Vue3 并未强制 provide/inject 必须响应式,而是让开发者自主选择:
    • 传递普通值(非响应式):用于静态配置。
    • 传递 ref/reactive 对象(响应式):用于动态状态共享。
      这种设计既保留了 Vue2 中 "配置传递" 的能力,又新增了 "响应式状态共享" 的选项,更符合多样化场景。
  4. 与状态管理库的互补
    Vue3 中,provide/inject 的响应式能力与 Pinia(官方状态管理库)并不冲突:
    • Pinia 适合全局共享的核心状态(如用户信息、购物车),提供更严格的规范(actions 修改状态)。
    • provide/inject 适合局部跨层级共享(如某个页面内的组件树),更轻量、无需定义全局 store。
      响应式支持让 provide/inject 在局部场景中更实用。

总结

  • Vue2 的 provide/inject 非响应式,是为了聚焦静态配置传递、避免数据流混乱,同时受限于当时的响应式系统实现。
  • Vue3 使其支持响应式,是响应式系统升级Composition API 场景扩展的自然结果,既保留了灵活性,又满足了动态状态共享的需求。

本质上,这是框架在 "设计约束" 和 "开发者需求" 之间的平衡调整 ------Vue3 通过更强大的底层能力,让 provide/inject 从 "单纯的依赖注入" 进化为 "灵活的跨层级通信工具"。

Vue 如何在监听上一行代码执行完后在执行后面得代码

在 Vue 中,若需要保证 "上一行代码执行完毕后再执行后续代码",核心取决于上一行代码是同步操作 还是异步操作。Vue 本身遵循 JavaScript 的执行机制,同步代码会按顺序执行,而异步代码(如 API 请求、定时器、DOM 更新等)需要通过特定方式等待其完成。以下是具体场景和解决方案:

一、同步代码:天然按顺序执行

如果上一行是同步代码(如变量赋值、简单计算、同步函数调用),JavaScript 会按代码顺序依次执行,无需额外处理。

javascript 复制代码
// 示例:同步代码天然顺序执行
const a = 1;
const b = a + 2; // 上一行执行完才会执行此行,b 一定是 3
console.log(b); // 输出 3

在 Vue 组件中(包括 <script setup>),同步代码同样遵循此规则:

vue 复制代码
<script setup>
import { ref } from 'vue';

const count = ref(0);

// 同步操作:按顺序执行
count.value = 1; // 第一步:修改值
const double = count.value * 2; // 第二步:使用修改后的值(double 一定是 2)
console.log(double); 
</script>

二、异步代码:需显式等待执行完成

如果上一行是异步操作 (如 API 请求、setTimeout、Promise 等),JavaScript 会跳过异步操作继续执行后续代码,此时需要通过 async/await.then() 确保异步操作完成后再执行后续逻辑。

场景 1:异步 API 请求(如 axios)

假设上一行是发送 API 请求,后续代码需要使用请求结果:

vue 复制代码
<script setup>
import { ref } from 'vue';
import axios from 'axios';

const data = ref(null);

// 错误示例:异步操作未等待,后续代码可能拿到 undefined
const res = axios.get('/api/data'); // 异步请求,不会阻塞后续代码
console.log(res.data); // 错误:此时请求未完成,res 是 Promise 对象

// 正确示例:使用 async/await 等待异步完成
const fetchData = async () => {
  // 上一行:等待 API 请求完成
  const res = await axios.get('/api/data'); 
  // 下一行:请求完成后才执行,可安全使用 res.data
  data.value = res.data; 
  console.log('数据获取成功:', data.value); 
};

fetchData();
</script>

场景 2:定时器或 Promise 异步操作

对于 setTimeout 或自定义 Promise 异步操作,同样需要通过 async/await 等待:

vue 复制代码
<script setup>
// 自定义异步函数(返回 Promise)
const delay = (ms) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('延迟完成');
    }, ms);
  });
};

// 使用 async/await 等待异步完成
const run = async () => {
  // 上一行:等待延迟完成
  const result = await delay(1000); 
  // 下一行:延迟结束后才执行
  console.log(result); // 输出 "延迟完成"
};

run();
</script>

三、等待 Vue 响应式更新或 DOM 渲染完成

在 Vue 中,修改响应式数据(如 refreactive)后,DOM 不会立即更新(Vue 会批量处理 DOM 更新以优化性能)。如果后续代码需要基于更新后的 DOM 状态执行(如获取 DOM 尺寸、位置),需要使用 nextTick

场景:修改数据后等待 DOM 更新

vue 复制代码
<template>
  <div ref="content">{{ message }}</div>
</template>

<script setup>
import { ref, nextTick } from 'vue';

const message = ref('初始文本');
const content = ref(null);

const updateMessage = async () => {
  // 上一行:修改响应式数据(DOM 不会立即更新)
  message.value = '更新后的文本'; 
  
  // 错误示例:直接获取 DOM,内容可能还是旧的
  console.log(content.value.textContent); // 可能输出 "初始文本"(DOM 未更新)

  // 正确示例:使用 nextTick 等待 DOM 更新完成
  await nextTick(); 
  // 下一行:DOM 已更新,可获取最新内容
  console.log(content.value.textContent); // 输出 "更新后的文本"
};

updateMessage();
</script>

原理nextTick 会在 Vue 完成当前批次 DOM 更新后执行回调,确保能获取到最新的 DOM 状态。

四、在 watch 中等待异步操作完成

如果在 watch 监听中需要等待上一行异步操作完成后再执行后续逻辑,可将 watch 回调定义为 async 函数,结合 await 使用。

vue 复制代码
<script setup>
import { ref, watch } from 'vue';
import axios from 'axios';

const userId = ref(1);
const userInfo = ref(null);

// 监听 userId 变化,等待 API 请求完成后再处理后续逻辑
watch(userId, async (newId) => {
  // 上一行:等待 API 请求完成
  const res = await axios.get(`/api/user/${newId}`); 
  // 下一行:请求完成后更新数据
  userInfo.value = res.data; 
  // 后续逻辑:基于 userInfo 执行
  console.log('用户名称:', userInfo.value.name); 
});
</script>

总结

确保 "上一行代码执行完后再执行后续代码" 的核心方案:

  1. 同步代码:无需处理,天然按顺序执行。
  2. 异步操作(API、定时器等) :使用 async/await.then() 等待异步完成。
  3. 等待 DOM 更新 :使用 nextTick 确保 DOM 已更新。
  4. watch 中处理异步 :将 watch 回调定义为 async 函数,通过 await 等待异步操作。

根据具体场景选择对应方案即可保证代码执行顺序。

vue nextTick的原理和应用场景

在 Vue 中,nextTick 是一个核心 API,用于在 DOM 更新完成后执行回调函数。理解其原理和应用场景对编写高效、稳定的 Vue 代码至关重要。

一、原理:Vue 的异步更新队列

  1. Vue 的 DOM 更新机制

Vue 的响应式系统在数据变化时,不会立即更新 DOM,而是将 DOM 更新任务异步批量处理。这是为了避免频繁操作 DOM 导致性能损耗(例如,多次修改同一个数据会合并为一次 DOM 更新)。

流程

  • 当数据变化时,Vue 会触发 setter 拦截器。
  • Vue 将对应的 DOM 更新任务(即 watcher)加入异步队列,而非立即执行。
  • 在下一次 "事件循环"(Event Loop)的微任务阶段,Vue 会清空队列并执行所有 DOM 更新。
  1. nextTick 的作用

nextTick 的回调函数会被添加到微任务队列末尾,确保在 DOM 更新完成后执行。其核心原理是:

  • 等待当前所有同步代码执行完毕。
  • 等待 Vue 的异步更新队列(微任务)执行完毕。
  • 执行 nextTick 的回调函数。

伪代码逻辑

javascript 复制代码
function nextTick(callback) {
  // 将回调添加到微任务队列
  if (Promise) {
    Promise.resolve().then(callback);
  } else {
    // 降级方案(兼容不支持 Promise 的环境)
    setTimeout(callback, 0);
  }
}

二、应用场景

  1. 在 DOM 更新后访问元素

当你修改数据后立即访问 DOM,此时 DOM 可能尚未更新,使用 nextTick 确保 DOM 已渲染。

react 与vue3得hooks得区别

React 和 Vue3 的 Hooks(或 Composition API)在设计理念、使用方式和底层机制上存在显著差异,核心区别体现在响应式模型、依赖追踪、函数调用规则逻辑组织方式上。以下从关键维度对比分析:

一、设计理念与核心目标

React Hooks

  • 目标:解决 class 组件的复用难题(如 HOC 嵌套地狱)、状态逻辑分散等问题,让函数组件拥有状态和生命周期能力。
  • 理念:基于 "函数式编程" 思想,强调 "每次渲染都是独立快照",通过纯函数抽象状态逻辑,避免 class 组件的 this 指向混乱。
  • 核心场景 :状态管理(useState)、副作用处理(useEffect)、逻辑复用(自定义 Hooks)。

Vue3 Composition API(类似 Hooks 的概念)

  • 目标:解决 Vue2 选项式 API(Options API)中逻辑复用困难(如 mixins 命名冲突、来源模糊)、复杂组件代码分散的问题。
  • 理念:基于 "响应式编程" 思想,通过组合函数(Composition Functions)将相关逻辑聚合,强调 "响应式数据驱动视图",与 Vue 底层响应式系统深度结合。
  • 核心场景 :响应式状态定义(ref/reactive)、副作用与依赖追踪(watch/watchEffect)、逻辑复用(组合函数)。

二、响应式模型与状态管理

React Hooks

  • 状态本质 :通过 useStateuseReducer 定义的状态是 "非响应式" 的,本质是函数组件的局部变量,状态更新会触发组件重新渲染(重新执行函数)。
  • 状态更新 :状态更新是 "替换式" 的(如 setCount(count + 1) 是生成新值替换旧值),对于引用类型(对象 / 数组),需手动创建新引用(如 setUser({...user, name: 'new'})),否则不会触发重渲染。
  • 访问方式 :直接访问变量(如 count),但每次渲染的状态是 "快照",闭包中捕获的是当前渲染周期的状态值。

Vue3 Composition API

  • 状态本质 :通过 ref(基本类型)或 reactive(对象类型)定义的状态是 "响应式" 的,底层基于 ES6 Proxy 实现,状态变化会自动触发依赖更新。
  • 状态更新 :状态更新是 "修改式" 的(如 count.value++user.name = 'new'),直接修改响应式对象的属性即可触发更新,无需替换整个对象(Proxy 会拦截修改操作)。
  • 访问方式ref 需通过 .value 访问 / 修改(模板中自动解包),reactive 直接访问属性(如 user.name),且始终能获取最新值(无闭包快照问题)。

三、副作用处理与依赖追踪

React Hooks(useEffect

  • 依赖显式声明useEffect 的执行时机由依赖数组 控制,必须手动指定依赖项(如 useEffect(() => {}, [count])),依赖变化时才会重新执行副作用。
  • 依赖追踪机制:无自动依赖追踪,完全依赖开发者手动维护依赖数组。若依赖遗漏,可能导致副作用捕获旧状态(闭包问题);若依赖冗余,可能导致不必要的重复执行。
  • 清理机制:副作用函数返回的清理函数会在组件卸载或依赖变化前执行(如取消订阅、清除定时器)。
  • 执行时机 :默认在 "浏览器绘制后" 执行(异步),可通过 { flush: 'sync' } 改为同步执行(不推荐)。

Vue3(watch/watchEffect

  • 依赖自动追踪watchEffect自动追踪副作用中使用的响应式数据,无需手动声明依赖。当这些响应式数据变化时,副作用自动重新执行(基于 Proxy 拦截访问)。
  • 精确监听watch 可显式指定监听源(如 watch(count, (newVal) => {})),支持监听单个 ref、reactive 对象属性或 getter 函数,依赖更可控。
  • 清理机制watchwatchEffect 的副作用函数可返回清理函数,在副作用重新执行前或组件卸载时自动调用。
  • 执行时机 :默认在 "组件更新后" 执行,可通过 flush: 'pre' 改为更新前执行(适合 DOM 操作)。

四、函数调用规则与限制

React Hooks

  • 严格调用顺序 :Hooks 必须在函数组件顶层调用 ,不能在条件语句、循环、嵌套函数中调用(如 if (flag) { useState() } 是错误的)。原因是 React 依赖 Hooks 的调用顺序来关联状态与组件,顺序错乱会导致状态匹配错误。
  • 唯一限制:必须在 React 函数组件或自定义 Hooks 中调用,否则会报错(React 内部通过上下文标记调用环境)。

Vue3 Composition API

  • 灵活调用位置refwatchonMounted 等函数可在 setup 函数或 <script setup>任意位置调用 ,包括条件语句、循环、嵌套函数中(如 if (flag) { const count = ref(0) } 是合法的)。原因是 Vue 的响应式依赖追踪基于 Proxy,与函数调用顺序无关,只关注实际使用的响应式数据。
  • 无严格环境限制 :只要在组件实例生命周期内(如 setup 执行期间),即可调用,无需强制在特定类型的函数中。

五、生命周期对应与逻辑组织

React Hooks

  • 生命周期模拟

    :通过

    复制代码
    useEffect

    模拟生命周期,例如:

    • 组件挂载:useEffect(() => {}, [])(空依赖);
    • 组件更新:useEffect(() => {}, [dep1, dep2])(依赖变化);
    • 组件卸载:useEffect(() => { return () => {} }, [])(清理函数)。
  • 逻辑组织 :按 "Hooks 调用顺序" 组织代码,同一逻辑的状态和副作用需放在相邻位置,复杂组件可能需要拆分多个自定义 Hooks(如 useUser()useForm())。

Vue3 Composition API

  • 生命周期显式化

    :提供专门的生命周期钩子(如

    复制代码
    onMounted
    
    onUpdated
    
    onUnmounted

    ),直接在

    复制代码
    setup

    中调用,语义更清晰:

    js 复制代码
    onMounted(() => { /* 挂载后执行 */ })
    onUnmounted(() => { /* 卸载时执行 */ })
  • 逻辑组织

    :按 "功能逻辑" 聚合代码,例如将 "用户信息加载与处理" 相关的

    复制代码
    ref
    
    watch
    
    onMounted

    放在同一个组合函数中,实现 "关注点分离":

    js 复制代码
    function useUser() {
      const user = ref(null)
      onMounted(() => { /* 加载用户 */ })
      watch(user, () => { /* 处理用户变化 */ })
      return { user }
    }

六、闭包问题与状态获取

React Hooks

  • 闭包陷阱

    :由于每次渲染是独立快照,

    复制代码
    useEffect

    中捕获的状态是 "当前渲染周期的值",若依赖数组未正确声明,可能导致副作用使用旧状态:

    js 复制代码
    const [count, setCount] = useState(0)
    useEffect(() => {
      setInterval(() => {
        console.log(count) // 始终打印 0,因未依赖 count
      }, 1000)
    }, []) // 错误:遗漏 count 依赖

    解决方式:需显式添加依赖

    复制代码
    [count]

    ,或用

    复制代码
    useRef

    保存最新值。

Vue3 Composition API

  • 无闭包陷阱

    :响应式数据(

    复制代码
    ref
    
    reactive

    )是 Proxy 代理对象,访问时始终获取最新值,即使在闭包中也不会捕获旧值:

    js 复制代码
    const count = ref(0)
    watchEffect(() => {
      setInterval(() => {
        console.log(count.value) // 始终打印最新值
      }, 1000)
    })

    原因是

    复制代码
    count.value

    访问的是 Proxy 拦截后的最新值,与闭包无关。

总结:核心差异对比表

维度 React Hooks Vue3 Composition API
响应式模型 非响应式,依赖重渲染更新 响应式(Proxy),自动追踪变化
状态更新方式 替换式(需创建新引用) 修改式(直接更新属性)
依赖追踪 显式依赖数组(需手动维护) 自动追踪(基于响应式访问)
函数调用限制 必须顶层调用(依赖顺序) 可任意位置调用(无关顺序)
闭包问题 易出现(依赖快照) 几乎无(始终访问最新值)
生命周期 useEffect 模拟 显式钩子(onMounted 等)

一句话总结

React Hooks 是 "函数式驱动的状态与副作用管理",依赖显式声明和严格调用规则;Vue3 Composition API 是 "响应式驱动的逻辑组合",依赖自动追踪和灵活调用方式,更贴近 Vue 原生响应式体系。

Proxy如何做的优化

在 Vue3 等框架中,使用 Proxy 实现响应式时,针对性能、内存占用和使用体验做了多项关键优化,核心思路是减少不必要的代理操作、精准追踪依赖、降低初始化成本。以下是具体的优化手段:

一、懒代理(Lazy Proxy):按需代理嵌套对象

Proxy 可以直接代理整个对象,但对于嵌套层级较深的对象(如 { a: { b: { c: 1 } } }),Vue3 不会一次性递归代理所有子对象,而是在访问子对象时才动态代理(懒加载思想)。

  • 优化点
    初始化时只代理顶层对象,避免对深层未访问的子对象做无用代理,大幅降低复杂对象的初始化时间和内存消耗。
  • 实现逻辑
    get 拦截器中,当访问的属性值是对象时,才对该子对象进行代理并缓存,后续访问直接复用已代理的子对象。
javascript 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      const value = Reflect.get(target, key);
      // 若属性值是对象,递归代理(懒代理)
      if (isObject(value)) {
        return reactive(value); 
      }
      // 依赖收集(简化版)
      track(target, key);
      return value;
    },
    // ...set等其他拦截器
  });
}

二、缓存机制:避免重复代理

对同一个对象多次调用 reactive 时,返回同一个代理对象(而非创建新 Proxy),避免重复代理导致的内存浪费和逻辑混乱。

  • 优化点
    WeakMap 缓存 "原始对象 → 代理对象" 的映射,既保证缓存复用,又不会阻止原始对象被垃圾回收(WeakMap 的键是弱引用)。
  • 实现逻辑
javascript 复制代码
const reactiveMap = new WeakMap(); // 缓存:原始对象 → 代理对象

function reactive(target) {
  // 若已代理过,直接返回缓存的代理对象
  const existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 否则创建新代理并缓存
  const proxy = new Proxy(target, handler);
  reactiveMap.set(target, proxy);
  return proxy;
}

三、精准拦截:只处理必要的操作

Proxy 的拦截器(getsetdeleteProperty 等)默认会拦截所有属性操作,但框架会通过过滤逻辑,只对 "有意义的操作" 进行拦截处理,减少无效计算。

  • 优化点:
    • 跳过对 Symbol 内置属性(如 Symbol.iterator)的拦截,避免干扰原生对象行为(如数组迭代)。
    • 跳过对不可配置、不可写属性的无意义拦截(如 Object.freeze 冻结的对象)。
    • 对数组的索引操作(如 arr[0] = 1)和原型方法(如 pushsplice)做特殊处理,避免全量拦截导致的性能损耗。

四、区分响应式类型:减少不必要的代理范围

Vue3 提供了 reactive(深层响应)、shallowReactive(浅层响应)、readonly(只读响应)等 API,通过不同的拦截器逻辑,精准控制响应式的范围:

  • shallowReactive:只代理顶层属性,不递归代理子对象,适合已知不会修改深层属性的场景(如配置对象),减少代理成本。
  • readonly :拦截 set 操作时直接报错(禁止修改),且不触发依赖更新,适合纯展示数据,避免无用的依赖追踪。
  • ref 对基本类型的优化 :对 numberstring 等基本类型,用 { value: ... } 包装后再代理,既兼容 Proxy(只能代理对象),又减少对原始值的不必要处理。

五、依赖追踪的精准化

响应式的核心是 "访问时收集依赖,修改时触发更新"。Proxy 配合 effect 系统实现了更精准的依赖追踪:

  • 只收集实际访问的属性
    例如访问 obj.a.b 时,只会收集 ab 的依赖,而不是整个 obj,修改 obj.c 时不会触发无关更新。
  • 避免重复收集依赖
    同一 effect 多次访问同一属性,只会记录一次依赖,减少依赖表的冗余。

六、跳过非响应式值的代理

对非对象类型(如基本类型、nullundefined)、Symbol、函数等,直接返回原始值,不创建 Proxy,避免无效操作:

javascript 复制代码
function reactive(target) {
  // 非对象类型直接返回,不代理
  if (!isObject(target)) {
    return target;
  }
  // ...后续代理逻辑
}

七、数组优化:高效拦截数组方法

数组的 pushpopsplice 等方法会修改数组本身,Vue3 对这些方法做了特殊处理:

  • 拦截并改写数组方法 :在 get 拦截器中,当访问数组的原型方法时,返回一个 "被包装的方法",该方法执行时会先触发原始操作,再通知依赖更新。
  • 避免索引遍历的性能损耗 :相比 Vue2 对数组索引的逐个拦截,Proxy 可以直接拦截数组方法,更高效地处理批量修改(如 arr.push(1,2,3))。

总结

Proxy 实现响应式的优化核心是 "按需处理" 和 "精准控制"

  • 通过懒代理、缓存减少初始化成本;
  • 通过区分响应式类型、过滤无效操作缩小代理范围;
  • 通过精准的依赖追踪减少更新时的无效触发。

这些优化让 Proxy 相比 Vue2 的 Object.defineProperty 在性能(尤其是复杂对象)和灵活性上有了质的提升,也让响应式系统更贴合实际开发中的场景需求。

相关内容

写下自己求职记录也给正在求职得一些建议-CSDN博客

从初中级如何迈入中高级-其实技术只是"入门卷"-CSDN博客

前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)-CSDN博客

前端梳理体系从常问问题去完善-工程篇(webpack,vite)_前端系统梳理-CSDN博客

前端梳理体系从常问问题去完善-框架篇(react生态)-CSDN博客

相关推荐
BBB努力学习程序设计8 分钟前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
BBB努力学习程序设计14 分钟前
CSS3渐变:用代码描绘色彩的流动之美
前端·html
冰暮流星23 分钟前
css之动画
前端·css
jump6801 小时前
axios
前端
spionbo1 小时前
前端解构赋值避坑指南基础到高阶深度解析技巧
前端
用户4099322502121 小时前
Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗
前端·ai编程·trae
开发者小天1 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者2 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
npm i / npm install 卡死不动解决方法
前端·npm·node.js
Kratzdisteln2 小时前
【Cursor _RubicsCube Diary 1】Node.js;npm;Vite
前端·npm·node.js