前言
对于Vue2,Vue3,我项目上用得不是很多,用得最多得还是React,也不知为啥,我是自学的Vue2出来得,然后出来找到得工作是React得,直到现在都是React,虽然中间也维护Vue得项目,写写Vue得项目还是可以得,不过嘛,框架原理大致都是相通得,所以整理得不像React那么细致。
对于知识体系,需要学一遍,之所以,以问题得方式去梳理就跟我们刷题一样,多刷才能记住嘛,通过提问得方式,去记住他,查缺补漏,这就是我为什么分享了四篇体系概念篇得原因,对于计算机网咯,还有移动端,小程序,这种虽然整理有,但不是很细致。有机会在分享吧。
Vue2&Vue3
Vue3 的defineProps,defineEmits,defineExpose
在 Vue3 的 <script setup>
语法糖中,defineProps
、defineEmits
、defineExpose
是三个核心的编译时宏 (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 中的 $emit
和 emits
选项),作用是定义事件的类型、参数 ,让父组件可以通过 @事件名
监听,同时提供类型校验和 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
声明的成员会被暴露,未声明的仍为私有。 - 避免过度耦合 :谨慎使用,过度暴露会导致组件间耦合度升高,优先通过
props
和emit
通信。 - TypeScript 支持 :可通过
defineComponent
或接口定义子组件暴露的类型,实现类型推断。
总结
defineProps
:处理父组件到子组件的数据输入,定义接收的 props 类型和默认值。defineEmits
:处理子组件到父组件的事件输出,声明可触发的事件及参数。defineExpose
:将子组件的内部成员暴露给父组件,用于特殊场景下的组件交互(谨慎使用)。
这三个 API 是 Vue3 <script setup>
语法中组件通信的核心工具,配合使用可实现清晰、类型安全的组件交互逻辑。
Vue3 watch和 watchEffect的区别
在 Vue3 中,watch
和 watchEffect
都是用于监听响应式数据变化并执行副作用的 API,但它们的设计理念和使用场景有显著区别。核心差异体现在监听方式、执行时机、依赖追踪等方面,具体如下:
1. 监听目标:明确指定 vs 自动追踪
-
watch
:需要明确指定监听的数据源watch
必须手动指定要监听的响应式数据(如ref
、reactive
的属性、计算属性等),只有当这些指定的数据源变化时,才会触发回调。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
配置改为立即执行)javascriptwatch(count, () => { console.log('count 变化了'); // 初始时不执行,只有 count 变了才执行 }, { immediate: true }); // 加上 immediate 后,初始化时会执行一次
-
watchEffect
:默认立即执行watchEffect
的回调在初始化时会立即执行一次(用于收集初始依赖),之后当依赖变化时再次执行。 (这是因为它需要通过首次执行来 "发现" 内部用到的响应式数据)
3. 回调参数:关注新旧值 vs 仅关注副作用
-
watch
的回调:接收新旧值watch
的回调函数有三个参数:newVal
(新值)、oldVal
(旧值)、onCleanup
(清理函数),适合需要对比数据变化前后状态的场景。javascriptwatch(count, (newVal, oldVal, onCleanup) => { // 对比新旧值 if (newVal > oldVal) { console.log('count 增加了'); } // 清理副作用(如防抖、取消请求) onCleanup(() => { // 下次回调执行前或组件卸载时触发 }); });
-
watchEffect
的回调:仅接收清理函数watchEffect
的回调只接收一个onCleanup
参数,不提供新旧值(因为它自动追踪依赖,无法精确知道哪个数据变化),适合只需要执行副作用(如发请求、更新 DOM)的场景。javascriptwatchEffect((onCleanup) => { // 发送请求(依赖 count) const timer = setTimeout(() => { console.log(`count 为 ${count.value}`); }, 1000); // 清理副作用(避免多次请求) onCleanup(() => clearTimeout(timer)); });
4. 依赖追踪精度:精确控制 vs 自动全量
-
watch
:可精确监听部分依赖 对于reactive
对象,watch
可以通过 getter 函数只监听某个具体属性,避免不必要的触发。javascriptconst user = reactive({ name: 'vue', age: 18 }); // 只监听 age 的变化(name 变化不会触发) watch(() => user.age, () => { console.log('只有年龄变化时触发'); });
-
watchEffect
:自动追踪所有依赖 只要回调函数中用到的响应式数据发生变化,无论是否是 "关键数据",都会触发回调。如果函数内用到多个数据,任何一个变化都会执行。javascriptconst user = reactive({ name: 'vue', age: 18 }); watchEffect(() => { // 用到了 name 和 age,任何一个变化都会触发 console.log(`${user.name} 的年龄是 ${user.age}`); });
5. 使用场景
场景 | 推荐 API | 原因 |
---|---|---|
需要知道数据变化的 "新旧值"(如表单验证、比较变化) | watch |
回调提供 newVal 和 oldVal ,方便对比 |
只需要在依赖变化时执行副作用(如发请求、更新 DOM) | watchEffect |
自动追踪依赖,代码更简洁 |
需精确控制监听的数据源(避免无关变化触发) | watch |
可手动指定监听目标,减少不必要的执行 |
初始化时需要立即执行一次副作用(如初始加载数据) | watchEffect |
默认立即执行,无需额外配置 |
总结
watch
是 "精确监听":需手动指定目标,懒执行,提供新旧值,适合需要精确控制和对比变化的场景。watchEffect
是 "自动追踪":无需指定目标,立即执行,不提供新旧值,适合简单的副作用场景,代码更简洁。
两者都可以通过返回的函数停止监听(const stop = watch(...)
或 const stop = watchEffect(...)
,调用 stop()
即可)。
从new Vue到页面实例使用经历那些步骤
- 创建项目
使用 Vue CLI
创建项目:
bash
# 安装 Vue CLI(如果未安装)
npm install -g @vue/cli
# 创建 Vue 2 项目
vue create my-vue2-app --default
cd my-vue2-app
npm install
- 项目结构概览
Vue 2 项目的核心结构:
plaintext
src/
├── App.vue # 根组件
├── main.js # 入口文件
├── components/ # 组件目录
├── router/ # 路由配置(可选)
└── store/ # Vuex 状态管理(可选)
- 定义组件(.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>
- 在 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>
- 入口文件配置(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');
- 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>
- 响应式原理
Vue 2 使用 Object.defineProperty()
javascript
export default {
data() {
return {
count: 0,
user: {
name: 'John',
age: 30
}
}
},
created() {
// 修改数据会触发视图更新
this.count++;
this.user.age = 31;
}
}
- 生命周期钩子
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('实例销毁之后被调用');
}
}
- 运行项目
bash
npm run serve # 开发环境
npm run build # 生产环境构建
关键概念(Vue 2 特有)
-
Options API :通过
data
、methods
、computed
等选项组织代码。 -
响应式限制
:
- 无法检测对象属性的添加或删除
- 数组通过特定方法触发更新(如
push()
、splice()
)
-
混合(Mixins):代码复用机制(Vue 3 推荐 Composition API)
-
过滤器(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 框架下的开发流程:
- 项目初始化:使用 Vue CLI 创建项目。
- 组件化开发:使用单文件组件(SFC)结构。
- Options API :通过
data
、methods
、computed
等选项组织代码。 - 实例创建与挂载 :通过
new Vue()
创建实例并挂载。 - 响应式更新 :基于
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。
核心流程:
- 数据劫持 :
- Vue 初始化时遍历
data
对象,将所有属性转换为getter/setter
。 - 每个属性关联一个 Dep(依赖收集器)。
- Vue 初始化时遍历
- 依赖收集 :
- 组件渲染时,访问数据触发
getter
,将当前渲染函数(Watcher)添加到 Dep 的依赖列表。
- 组件渲染时,访问数据触发
- 通知更新 :
- 数据变更时触发
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。
核心流程:
- VNode 生成 :
- 模板编译或手写
render
函数生成 VNode 树。
- 模板编译或手写
- Diff 算法 :
- 新旧 VNode 对比,找出最小变更集。
- 采用 双指针 + key 追踪 优化性能。
- 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 组件是独立的实例,拥有自己的生命周期和作用域。
核心机制:
- 组件实例化 :
- 每个组件都是
Vue
构造函数的实例。 - 组件间通过
props
和events
通信。
- 每个组件都是
- 生命周期钩子 :
- 关键阶段:
beforeCreate
→created
→beforeMount
→mounted
→beforeUpdate
→updated
→beforeDestroy
→destroyed
。
- 关键阶段:
- 异步渲染队列 :
- 多次数据变更会合并为一次 DOM 更新,通过
nextTick
访问更新后的 DOM。
- 多次数据变更会合并为一次 DOM 更新,通过
生命周期简化流程图:
plaintext
创建实例 → 初始化数据 → 编译模板 → 挂载 DOM → 数据变更 → 虚拟 DOM diff → 更新 DOM → 销毁实例
四、模板编译
Vue 将模板字符串编译为 render
函数,生成 VNode。
编译流程:
- 解析(Parse) :
- 将模板字符串转换为 AST(抽象语法树)。
- 优化(Optimize) :
- 标记静态节点,避免重复 diff。
- 生成(Generate) :
- 将 AST 转换为
render
函数代码。
- 将 AST 转换为
示例:
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 核心改进
- Proxy 响应式系统 :
- 解决 Vue 2.x 对象新增属性、数组索引修改等限制。
- 组合式 API(Composition API) :
- 通过
setup()
函数实现逻辑复用和代码组织。
- 通过
- Tree-Shaking 支持 :
- 按需打包,减小生产包体积。
- 性能优化 :
- 更高效的 Diff 算法(PatchFlag)、静态提升(Static Hoisting)等。
六、常见面试问题解答
- Vue 响应式原理是什么?
- Vue 通过
Object.defineProperty()
或Proxy
劫持数据的getter/setter
,结合依赖收集和发布订阅模式实现。
- Vue 通过
- Vue 如何检测数组变化?
- Vue 2.x 通过重写数组原型方法(如
push
,pop
)实现监听,Vue 3.x 直接使用Proxy
。
- Vue 2.x 通过重写数组原型方法(如
- 虚拟 DOM 的优缺点?
- 优点:减少 DOM 操作次数、跨平台支持(SSR、移动端);
- 缺点:首次渲染效率较低、复杂场景 diff 算法可能耗时。
- Vue 生命周期钩子的作用?
- 例如
created
用于数据初始化,mounted
用于 DOM 操作,beforeDestroy
用于清理资源。
- 例如
总结
Vue 的核心优势在于响应式系统 的优雅设计、虚拟 DOM 的高效更新和组件化的开发模式。理解这些原理有助于写出更高效、可维护的 Vue 应用,同时也能更好地应对性能优化和疑难问题排查。
Vue2 和 Vue3的区别
Vue 2 和 Vue 3 是 Vue.js 框架的两个主要版本,Vue 3 在保持与 Vue 2 兼容性的同时引入了多项重大改进。以下是它们的核心区别及升级建议:
一、架构与性能
- 响应式系统
-
Vue 2
-
基于
cssObject.defineProperty()
实现,存在以下限制:
- 无法检测对象属性的添加或删除。
- 数组通过劫持原型方法实现响应式,部分操作(如直接通过索引修改)需手动处理。
-
-
Vue 3
- 使用 ES6 Proxy 重构响应式系统,解决了上述问题:
- 可以检测对象属性的添加 / 删除。
- 对数组操作的响应式支持更全面。
- 性能提升约 2 倍(更少的依赖追踪开销)。
- 使用 ES6 Proxy 重构响应式系统,解决了上述问题:
- 编译优化
-
Vue 3
-
引入
Block Tree
和
Patch Flag
等编译时优化:
- 静态内容不再参与虚拟 DOM 比较,提升渲染效率。
- 动态绑定标记更精确,仅更新变化的部分。
-
- 体积优化
- Vue 3
- 通过 Tree-Shaking 移除未使用的代码,核心体积减少约 41%。
二、API 设计
- 组合式 API(Composition API)
-
Vue 3
-
新增
Composition API
(基于
scsssetup()
函数或
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)的以下问题:
- 代码复用困难 :逻辑分散在
data
、methods
、computed
等选项中。 - 大型组件难以维护:相关逻辑被拆分在不同选项,导致 "碎片化"。
- 代码复用困难 :逻辑分散在
-
- 选项式 API 的变化
- Vue 3
- 保留大部分选项式 API,但有以下调整:
data
必须是函数。- 生命周期钩子改名(如
beforeDestroy
→beforeUnmount
)。 - 新增
setup()
选项作为组件的入口点。
- 保留大部分选项式 API,但有以下调整:
三、组件与模块
- 多根组件(Fragment)
-
Vue 3
-
组件可以有多个根节点
vue<template> <header>...</header> <main>...</main> <footer>...</footer> </template>
-
- Teleport(传送门)
-
Vue 3
-
允许将组件渲染到 DOM 中的其他位置:
vue<teleport to="body"> <div class="modal">...</div> </teleport>
-
- Suspense(异步组件)
-
Vue 3
-
内置对异步组件的支持:
vue<Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <LoadingSpinner /> </template> </Suspense>
-
四、TypeScript 支持
-
Vue 2
- 需要通过
vue-class-component
和vue-property-decorator
等插件支持 TypeScript,集成不够自然。
- 需要通过
-
Vue 3
-
从底层设计支持 TypeScript,组合式 API 提供了更友好的类型推导:
typescriptimport { ref, computed } from 'vue'; const count = ref(0); // 类型自动推断为 Ref<number> const double = computed(() => count.value * 2); // Ref<number>
-
五、生态与兼容性
- Vue Router
-
Vue 3
-
需要使用 Vue Router 4.x,支持组合式 API:
typescriptimport { useRoute, useRouter } from 'vue-router'; const route = useRoute(); const router = useRouter();
-
- Vuex
- Vue 3
- 推荐使用 Pinia 替代 Vuex,提供更简洁的 API 和更好的 TypeScript 支持。
- 插件与库
- 部分 Vue 2 插件需要更新后才能兼容 Vue 3(如
vuex-persistedstate
)。
六、升级建议
- 新项目
- 优先选择 Vue 3 + TypeScript + 组合式 API,充分利用新特性和性能优化。
- 现有项目升级
- 渐进式迁移 :使用 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 在底层实现上有差异,但整体思路一致。
一、核心原理:双向绑定的 "双向" 拆解
双向绑定的本质是 "两个单向绑定的结合":
- 数据 → 视图:数据变化时,自动更新视图(依赖响应式系统和视图更新机制)。
- 视图 → 数据:视图变化时(如用户输入),自动同步数据(依赖事件监听)。
二、数据 → 视图:数据驱动视图的原理
当数据发生变化时,Vue 能自动更新视图,核心依赖 响应式系统 和 依赖收集机制。
1. 响应式系统:数据劫持(监听数据变化)
Vue 通过 "劫持" 数据的读取和修改操作,实现对数据变化的感知。
-
Vue 2 实现:
Object.defineProperty()
Vue 2 对数据(data
中的对象)的每个属性通过Object.defineProperty()
重写getter
和setter
:getter
:当属性被读取时触发,用于收集依赖(记录 "谁在使用这个数据")。setter
:当属性被修改时触发,用于通知依赖更新(告诉 "使用这个数据的地方" 重新渲染)。
示例简化代码
javascriptfunction 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
)。
示例简化代码:
javascriptfunction 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
替代Watcher
,track
函数收集依赖(记录当前活跃的Effect
),trigger
函数触发所有关联的Effect
执行。
- 用
3. 视图更新:从数据变化到 DOM 渲染
当数据变化触发 setter
(Vue 2)或 Proxy.set
(Vue 3)后,会通过以下流程更新视图:
- 通知所有依赖(
Watcher
/Effect
)"数据变了"。 - 依赖触发更新函数(如组件的渲染函数),生成新的虚拟 DOM(VNode)。
- 通过虚拟 DOM 的 diff 算法 对比新旧 VNode,计算出最小更新范围。
- 将差异应用到真实 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 双向数据绑定的核心逻辑是:
- 通过 数据劫持 (
Object.defineProperty
或Proxy
)感知数据变化。 - 通过 依赖收集 (
Watcher
或Effect
)记录数据与视图的关联。 - 数据变化时,通过 虚拟 DOM diff 更新视图(数据 → 视图)。
- 视图变化时,通过 事件监听 同步数据(视图 → 数据)。
这一机制让开发者无需手动操作 DOM,只需关注数据逻辑,大幅提升开发效率。
虚拟DOM和真实DOM的区别
虚拟 DOM(Virtual DOM)和真实 DOM(Real DOM)是前端开发中的两个重要概念,它们的主要区别如下:
- 定义与结构
- 真实 DOM 是由浏览器提供的 API,是文档的树形结构表示,每个节点都是一个对象,直接与浏览器渲染引擎交互。操作真实 DOM 的代价很高,因为每次修改都会触发浏览器的重排(reflow)和重绘(repaint)。
- 虚拟 DOM 是真实 DOM 的抽象表示,通常用 JavaScript 对象或轻量级数据结构(如 React 中的
element
)来模拟 DOM 树。它是真实 DOM 的 "虚拟映射",不直接参与渲染。
- 性能差异
- 真实 DOM 直接操作会导致频繁的重排和重绘,性能开销大,尤其在复杂应用中容易出现卡顿。
- 虚拟 DOM 通过批量计算差异(Diff 算法),只将必要的变更一次性应用到真实 DOM 上,减少渲染次数,提高性能。
- 操作方式
-
真实 DOM 操作直接影响页面,例如:
javascriptdocument.getElementById('app').innerHTML = '<div>Hello World</div>';
-
虚拟 DOM 通过状态变化触发重新渲染,框架内部计算差异后更新真实 DOM,例如 React 中的 JSX:
jsxfunction App() { return <div>Hello World</div>; }
- 应用场景
- 真实 DOM 适合简单交互或直接操作特定元素的场景(如动画、临时 UI 更新)。
- 虚拟 DOM 适合复杂 UI 和频繁更新的应用(如单页应用),通过减少 DOM 操作提升效率。
- 典型框架
- 真实 DOM 原生 JavaScript、jQuery 等直接操作 DOM 的库。
- 虚拟 DOM React、Vue.js(2.x 及以后)、Angular 等现代框架。
总结
虚拟 DOM 通过抽象和解耦 DOM 操作,减少了直接操作真实 DOM 的性能损耗,尤其在大型应用中优势明显。但它并非银弹,在简单场景下可能带来额外的复杂度。
前端Key有什么作用
在前端开发中,key
是一个特殊的属性(常见于 React、Vue 等框架),主要用于优化列表渲染性能和确保 DOM 元素的正确识别。其核心作用如下:
1. 标识列表项的唯一性
当渲染列表(如 v-for
或 map
生成的元素)时,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]
,此时 A
的 key
从 0
变为 1
,框架会误判 A
是新元素并重建,导致性能损耗。
3. 确保组件状态的正确复用
对于列表中的组件,key
决定组件实例是否复用:
- 若
key
不变,组件实例会被复用(保留内部状态,如data
、state
)。 - 若
key
变化,组件会被销毁并重新创建(重置内部状态)。
vue
<!-- Vue 中通过改变 key 重置组件 -->
<my-component :key="activeTab" />
4. 避免渲染错误
在某些场景下,缺少 key
可能导致渲染异常:
- 列项包含表单元素(如
input
)时,可能出现输入值与显示内容不匹配。 - 动画或过渡效果错乱(框架无法正确识别元素的插入 / 删除状态)。
使用 key
的注意事项
- 唯一性 :
key
在当前列表中必须唯一,不能重复(否则会报错或导致渲染异常)。 - 稳定性 :
key
应尽量稳定(如用后端返回的id
),避免使用易变的值(如索引index
或随机数)。 - 作用域 :
key
的唯一性仅针对当前列表,不同列表的key
可以重复。
总结
key
的核心作用是帮助框架高效识别列表项的身份 ,从而优化 DOM 更新性能、确保组件状态正确复用。 开发中应优先使用数据自身的唯一标识 (如 id
)作为 key
,避免滥用索引或随机值。
怎么判断一个空对象,空数组
- 判断空对象 :优先使用
Object.keys(obj).length === 0
,若需包含不可枚举属性,使用Reflect.ownKeys()
。 - 判断空数组 :直接使用
arr.length === 0
,若需严格校验元素,结合Array.every()
。 - 通用场景 :使用
lodash.isEmpty()
或自定义函数结合多种方法。
- 后端接口返回的数据为空时,前端怎么判断
前端判断后端返回的 "空数据",核心是:
- 明确 "空" 的具体形式(结合接口文档);
- 针对性判断 (区分
null
、空对象、空数组等); - 避免误判 (如不把
0
或false
当作空值)。
合理的空值判断能提升代码健壮性,避免因数据异常导致的页面崩溃或逻辑错误。
-
Vue2 set 是什么,用来做什么
在 Vue2 中,
Vue.set
(或其别名this.$set
)是一个全局 API,用于解决 Vue2 响应式系统的深层响应式限制 。它的主要作用是向响应式对象中添加一个新属性,并确保这个新属性同样具备响应式能力。为什么需要 Vue.set?
Vue2 的响应式系统基于 Object.defineProperty() 实现,它在初始化时会递归遍历 data 对象的所有属性,将其转换为
getter/setter
。但这种方式存在两个限制:- 无法检测对象属性的添加或删除 当你直接给响应式对象添加一个新属性时(如
this.obj.newProp = 'value'
),Vue2 无法自动将这个新属性转换为响应式的,因此不会触发视图更新。 - 无法检测数组索引的直接修改 当你通过索引直接修改数组元素时(如
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
,还有一些场景可以使用其他方式实现响应式更新:
-
替换整个对象 如果需要添加多个属性,可以用新对象替换原对象:
javascript// 替换整个 user 对象,确保所有新属性都是响应式的 this.user = { ...this.user, // 保留原有属性 age: 30, gender: 'male' }
-
使用数组变异方法 Vue2 对数组的某些方法(如
push
、splice
)做了响应式处理: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 的核心概念、工作流程及应用场景的详细介绍:
一、核心概念
- State
- 单一数据源 :应用的所有状态集中存储在一个
store
中。 - 响应式:Vuex 的 state 变化会自动触发视图更新。
- Getter
- 计算属性 :类似于 Vue 组件中的
computed
,用于获取 state 的派生数据。 - 缓存机制:依赖的 state 不变时,多次调用不会重复计算。
- Mutation
- 唯一修改途径:修改 state 必须通过提交 mutation。
- 同步操作:确保状态变化可追踪和调试。
- Action
- 异步操作:处理异步逻辑(如 API 请求),完成后提交 mutation。
- 分发(dispatch) :通过
store.dispatch()
触发。
- Module
- 模块化:将 store 分割成多个模块,每个模块有自己的 state、mutation、action 等。
二、工作流程
plaintext
组件触发 Action(异步操作) → Action 提交 Mutation → Mutation 修改 State → State 变化触发视图更新
关键流程说明:
-
组件中触发 Action
javascriptthis.$store.dispatch('fetchUserInfo');
-
Action 处理异步逻辑
javascriptactions: { fetchUserInfo({ commit }) { api.getUser().then(data => { commit('SET_USER', data); // 提交 mutation }); } }
-
Mutation 修改 State
javascriptmutations: { SET_USER(state, user) { state.user = user; // 直接修改 state } }
-
组件获取 State
javascriptcomputed: { user() { return this.$store.state.user; } }
三、应用场景
- 多组件共享状态
- 如用户信息、主题设置、购物车数据等。
- 复杂数据流管理
- 多级组件嵌套通信(替代事件总线或 prop 层层传递)。
- 状态持久化
- 结合插件(如
vuex-persistedstate
)将 state 存储到本地存储或会话存储。
- 结合插件(如
- 服务端渲染(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>
五、高级特性
-
模块化
javascript// store/modules/cart.js export default { namespaced: true, // 启用命名空间 state: { items: [] }, mutations: { /* ... */ }, actions: { /* ... */ }, getters: { /* ... */ } }; // 在主 store 中引入模块 new Vuex.Store({ modules: { cart: cartModule } });
-
插件
- 日志插件:记录 mutation 变化。
- 持久化插件:自动同步 state 到本地存储。
-
严格模式
- 开发环境中强制所有 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
限制响应式层级,或通过分离对象将非监听属性放在普通对象中。
两种方式都能实现只监听 a
和 c
,而忽略 d
的变化。
Vue2 provide/inject
为什么有意设计不是响应式,而Vue3又是
Vue2 中 provide/inject
设计为非响应式,而 Vue3 使其支持响应式,本质上是由框架设计理念、响应式系统实现差异 以及使用场景定位共同决定的,具体原因如下:
一、Vue2 中 provide/inject
非响应式的设计考量
- 核心定位:依赖注入而非状态管理 Vue2 中
provide/inject
的设计初衷是解决深层嵌套组件间的配置传递问题(如主题、国际化配置等静态 / 低频变动的数据),而非用于动态状态共享。 例如:一个组件库的底层组件可能需要获取顶层组件的配置(如是否禁用动画),这类配置通常在初始化后不会频繁变化,因此无需响应式。 - 避免滥用导致的数据流混乱 Vue2 强调 "单向数据流"(父→子通过 props,子→父通过事件),而
provide/inject
本质上是 "跨层级透传",如果支持响应式,可能会导致:- 开发者过度依赖它实现状态共享,替代 Vuex 或 props,使数据流向变得隐蔽(难以追踪谁修改了数据)。
- 响应式依赖链变长,增加调试复杂度(比如深层组件修改注入的数据,难以定位来源)。
- 响应式系统的技术限制 Vue2 的响应式基于
Object.defineProperty
,对对象 / 数组的拦截存在天然限制(如无法监听新增属性、数组索引修改等)。若要让provide/inject
支持响应式,需要手动将数据包装为Vue.observable
(Vue2 中让对象响应式的方法),但这会增加使用成本,且不符合其 "轻量配置传递" 的定位。
二、Vue3 中 provide/inject
支持响应式的原因
- 响应式系统的底层升级 Vue3 改用
Proxy
实现响应式,能原生支持对对象、数组的完整拦截(包括新增属性、删除属性等),且响应式数据的包装(ref
/reactive
)更轻量、直观。 这使得provide/inject
可以自然地传递响应式数据 ------ 只需将ref
或reactive
对象通过provide
传递,inject
后即可直接触发响应式更新,无需额外处理。 - 使用场景的扩展:配合 Composition API Vue3 引入的 Composition API 鼓励将逻辑按功能拆分(而非按选项),这使得跨组件共享 "带响应式的业务逻辑" 成为常见需求。 例如:在组件树中共享用户登录状态(
user = ref(null)
),需要在多个层级的组件中实时响应状态变化。此时provide/inject
作为轻量级跨层级通信方案,必须支持响应式才能满足需求。 - 更灵活的 "响应式控制权" Vue3 并未强制
provide/inject
必须响应式,而是让开发者自主选择:- 传递普通值(非响应式):用于静态配置。
- 传递
ref
/reactive
对象(响应式):用于动态状态共享。 这种设计既保留了 Vue2 中 "配置传递" 的能力,又新增了 "响应式状态共享" 的选项,更符合多样化场景。
- 与状态管理库的互补 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 中,修改响应式数据(如 ref
、reactive
)后,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>
总结
确保 "上一行代码执行完后再执行后续代码" 的核心方案:
- 同步代码:无需处理,天然按顺序执行。
- 异步操作(API、定时器等) :使用
async/await
或.then()
等待异步完成。 - 等待 DOM 更新 :使用
nextTick
确保 DOM 已更新。 - watch 中处理异步 :将 watch 回调定义为
async
函数,通过await
等待异步操作。
根据具体场景选择对应方案即可保证代码执行顺序。
vue nextTick的原理和应用场景
在 Vue 中,nextTick
是一个核心 API,用于在 DOM 更新完成后执行回调函数。理解其原理和应用场景对编写高效、稳定的 Vue 代码至关重要。
一、原理:Vue 的异步更新队列
- Vue 的 DOM 更新机制
Vue 的响应式系统在数据变化时,不会立即更新 DOM,而是将 DOM 更新任务异步批量处理。这是为了避免频繁操作 DOM 导致性能损耗(例如,多次修改同一个数据会合并为一次 DOM 更新)。
流程:
- 当数据变化时,Vue 会触发
setter
拦截器。 - Vue 将对应的 DOM 更新任务(即 watcher)加入异步队列,而非立即执行。
- 在下一次 "事件循环"(Event Loop)的微任务阶段,Vue 会清空队列并执行所有 DOM 更新。
- nextTick 的作用
nextTick
的回调函数会被添加到微任务队列末尾,确保在 DOM 更新完成后执行。其核心原理是:
- 等待当前所有同步代码执行完毕。
- 等待 Vue 的异步更新队列(微任务)执行完毕。
- 执行
nextTick
的回调函数。
伪代码逻辑:
javascript
function nextTick(callback) {
// 将回调添加到微任务队列
if (Promise) {
Promise.resolve().then(callback);
} else {
// 降级方案(兼容不支持 Promise 的环境)
setTimeout(callback, 0);
}
}
二、应用场景
- 在 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
- 状态本质 :通过
useState
或useReducer
定义的状态是 "非响应式" 的,本质是函数组件的局部变量,状态更新会触发组件重新渲染(重新执行函数)。 - 状态更新 :状态更新是 "替换式" 的(如
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 函数,依赖更可控。 - 清理机制 :
watch
和watchEffect
的副作用函数可返回清理函数,在副作用重新执行前或组件卸载时自动调用。 - 执行时机 :默认在 "组件更新后" 执行,可通过
flush: 'pre'
改为更新前执行(适合 DOM 操作)。
四、函数调用规则与限制
React Hooks
- 严格调用顺序 :Hooks 必须在函数组件顶层调用 ,不能在条件语句、循环、嵌套函数中调用(如
if (flag) { useState() }
是错误的)。原因是 React 依赖 Hooks 的调用顺序来关联状态与组件,顺序错乱会导致状态匹配错误。 - 唯一限制:必须在 React 函数组件或自定义 Hooks 中调用,否则会报错(React 内部通过上下文标记调用环境)。
Vue3 Composition API
- 灵活调用位置 :
ref
、watch
、onMounted
等函数可在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
),直接在
arduinosetup
中调用,语义更清晰:
jsonMounted(() => { /* 挂载后执行 */ }) onUnmounted(() => { /* 卸载时执行 */ })
-
逻辑组织
:按 "功能逻辑" 聚合代码,例如将 "用户信息加载与处理" 相关的
csharpref
watch onMounted
放在同一个组合函数中,实现 "关注点分离":
jsfunction useUser() { const user = ref(null) onMounted(() => { /* 加载用户 */ }) watch(user, () => { /* 处理用户变化 */ }) return { user } }
六、闭包问题与状态获取
React Hooks
-
闭包陷阱
:由于每次渲染是独立快照,
useEffect
中捕获的状态是 "当前渲染周期的值",若依赖数组未正确声明,可能导致副作用使用旧状态:
jsconst [count, setCount] = useState(0) useEffect(() => { setInterval(() => { console.log(count) // 始终打印 0,因未依赖 count }, 1000) }, []) // 错误:遗漏 count 依赖
解决方式:需显式添加依赖
csharp[count]
,或用
useRef
保存最新值。
Vue3 Composition API
-
无闭包陷阱
:响应式数据(
csharpref
reactive
)是 Proxy 代理对象,访问时始终获取最新值,即使在闭包中也不会捕获旧值:
jsconst 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
的拦截器(get
、set
、deleteProperty
等)默认会拦截所有属性操作,但框架会通过过滤逻辑,只对 "有意义的操作" 进行拦截处理,减少无效计算。
- 优化点:
- 跳过对
Symbol
内置属性(如Symbol.iterator
)的拦截,避免干扰原生对象行为(如数组迭代)。 - 跳过对不可配置、不可写属性的无意义拦截(如
Object.freeze
冻结的对象)。 - 对数组的索引操作(如
arr[0] = 1
)和原型方法(如push
、splice
)做特殊处理,避免全量拦截导致的性能损耗。
- 跳过对
四、区分响应式类型:减少不必要的代理范围
Vue3 提供了 reactive
(深层响应)、shallowReactive
(浅层响应)、readonly
(只读响应)等 API,通过不同的拦截器逻辑,精准控制响应式的范围:
shallowReactive
:只代理顶层属性,不递归代理子对象,适合已知不会修改深层属性的场景(如配置对象),减少代理成本。readonly
:拦截set
操作时直接报错(禁止修改),且不触发依赖更新,适合纯展示数据,避免无用的依赖追踪。ref
对基本类型的优化 :对number
、string
等基本类型,用{ value: ... }
包装后再代理,既兼容Proxy
(只能代理对象),又减少对原始值的不必要处理。
五、依赖追踪的精准化
响应式的核心是 "访问时收集依赖,修改时触发更新"。Proxy
配合 effect
系统实现了更精准的依赖追踪:
- 只收集实际访问的属性 : 例如访问
obj.a.b
时,只会收集a
和b
的依赖,而不是整个obj
,修改obj.c
时不会触发无关更新。 - 避免重复收集依赖 : 同一
effect
多次访问同一属性,只会记录一次依赖,减少依赖表的冗余。
六、跳过非响应式值的代理
对非对象类型(如基本类型、null
、undefined
)、Symbol
、函数等,直接返回原始值,不创建 Proxy
,避免无效操作:
javascript
function reactive(target) {
// 非对象类型直接返回,不代理
if (!isObject(target)) {
return target;
}
// ...后续代理逻辑
}
七、数组优化:高效拦截数组方法
数组的 push
、pop
、splice
等方法会修改数组本身,Vue3 对这些方法做了特殊处理:
- 拦截并改写数组方法 :在
get
拦截器中,当访问数组的原型方法时,返回一个 "被包装的方法",该方法执行时会先触发原始操作,再通知依赖更新。 - 避免索引遍历的性能损耗 :相比 Vue2 对数组索引的逐个拦截,
Proxy
可以直接拦截数组方法,更高效地处理批量修改(如arr.push(1,2,3)
)。
总结
Proxy
实现响应式的优化核心是 "按需处理" 和 "精准控制":
- 通过懒代理、缓存减少初始化成本;
- 通过区分响应式类型、过滤无效操作缩小代理范围;
- 通过精准的依赖追踪减少更新时的无效触发。
这些优化让 Proxy
相比 Vue2 的 Object.defineProperty
在性能(尤其是复杂对象)和灵活性上有了质的提升,也让响应式系统更贴合实际开发中的场景需求。
相关内容
从初中级如何迈入中高级-其实技术只是"入门卷"-CSDN博客
前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)-CSDN博客