前端梳理体系从常问问题去完善-框架篇(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

    • 基于

      css 复制代码
      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

      (基于

      scss 复制代码
      setup()

      函数或

      xml 复制代码
      <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

在输入框(如

css 复制代码
<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);

注意事项

javascript 复制代码
  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

    ),直接在

    arduino 复制代码
    setup

    中调用,语义更清晰:

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

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

    csharp 复制代码
    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 依赖

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

    csharp 复制代码
    [count]

    ,或用

    复制代码
    useRef

    保存最新值。

Vue3 Composition API

  • 无闭包陷阱

    :响应式数据(

    csharp 复制代码
    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博客

相关推荐
嫂子的姐夫3 小时前
11-py调用js
javascript·爬虫·python·网络爬虫·爬山算法
小墨宝3 小时前
web前端学习LangGraph
前端·学习
南囝coding3 小时前
React 19.2 重磅更新!这几个新特性终于来了
前端·react.js·preact
Dajiaonew3 小时前
Vue3 + TypeScript 一篇文章 后端变全栈
前端·javascript·typescript
广州华水科技4 小时前
GNSS与单北斗变形监测一体机在基础设施安全中的应用分析
前端
勤劳打代码4 小时前
妙笔生花 —— Flutter 实现飞入动画
前端·flutter·设计模式
银安4 小时前
CSS排版布局篇(4):浮动(float)、定位(position) 、层叠(Stacking)
前端·css
昭昭日月明5 小时前
mac 效率工具:Raycast 的扩展开发
前端·mac·设计
white-persist5 小时前
XXE 注入漏洞全解析:从原理到实战
开发语言·前端·网络·安全·web安全·网络安全·信息可视化