一、 响应式原理差异
1、核心实现
// Vue2 使用 Object.defineProperty 来实现响应式,这是 ES5 的特性
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 通知更新
dep.notify();
}
});
}
// Vue3 使用`Proxy`和`Reflect`来实现响应式,这是 ES6 的特性
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key); // 触发更新
return result;
}
});
}
2、vue2 响应式原理
基于
Object.defineProperty
实现,存在一些限制,如无法检测数组索引变化、对数组的处理需要特殊方法(如push
,pop
,splice
),无法检测对象属性的添加或删除
3、vue3 响应式原理
基于 ES6 的
Proxy和Reflect
实现可以检测到数组索引变化
可以检测到对象属性的添加或删除
性能更好,无需递归遍历对象所有属性
支持 Map、Set、WeakMap 和 WeakSet
二、 生命周期钩子变化
1、钩子名称调整
// Vue2
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}
// Vue3
export default {
setup() {}, // 替代beforeCreate和created
onBeforeMount() {},
onMounted() {},
onBeforeUpdate() {},
onUpdated() {},
onBeforeUnmount() {}, // 替代beforeDestroy
onUnmounted() {} // 替代destroyed
}
三、 模板语法变化
1、v-model 变化
// Vue 2 中 v-model 本质是 value prop 和 input 事件
<ChildComponent v-model="pageTitle" />
<!-- 等价于 -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
// Vue 3 中 v-model 改为 modelValue prop 和 update:modelValue 事件
<ChildComponent v-model="pageTitle" />
<!-- 等价于 -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
2、支持多个 v-model
<!-- Vue2 -->
<input v-model="message">
<!-- Vue3 -->
<MyComponent v-model:value="message" v-model:title="title" />
3、支持多个事件处理器
<!-- Vue2 -->
<button @click="handleClick">点击</button>
<!-- Vue3 - 支持多个事件处理器 -->
<button @click="handleClick1" @click="handleClick2">点击</button>
4、 片段支持
<!-- Vue2 - 需要1个根节点 -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- Vue3 - 支持多根节点 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
5、允许在 template 上使用 key
<!-- Vue 2 -->
<li v-for="item in list" :key="item.id">
{{ item.text }}
</li>
<!-- Vue 3 -->
<li v-for="item in list" :key="item.id">
{{ item.text }}
</li>
<!-- 但 Vue 3 允许在 template 上使用 key -->
<template v-for="item in list" :key="item.id">
<li>{{ item.text }}</li>
</template>
四、异步组件的定义
// vue2
new Vue({
components: {
AsyncComponent: () => import('./AsyncComponent.vue')
}
})
// vue3
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
五、 渲染函数的变化
// vue2
export default {
render(h) {
return h('div', {
attrs: {
id: 'foo'
},
on: {
click: this.onClick
}
}, 'hello')
}
}
// vue3
import { h } from 'vue'
export default {
render() {
return h('div', {
id: 'foo',
onClick: this.onClick
}, 'hello')
}
}
六、过渡类名变化
Vue 2 过渡类名:
.v-enter
进入过渡的开始状态
.v-enter-active
进入过渡的激活状态
.v-enter-to
进入过渡的结束状态
.v-leave
离开过渡的开始状态
.v-leave-active
离开过渡的激活状态
.v-leave-to
离开过渡的结束状态Vue 3 过渡类名:
.v-enter-from
进入过渡的开始状态
.v-enter-active
进入过渡的激活状态
.v-enter-to
进入过渡的结束状态
.v-leave-from
离开过渡的开始状态
.v-leave-active
离开过渡的激活状态
.v-leave-to
离开过渡的结束状态
七、 组件通信
// Vue2
export default {
props: ['title'],
methods: {
handleClick() {
this.$emit('update', newValue)
}
}
}
// Vue3
import { defineProps, defineEmits } from 'vue'
const props = defineProps(['title'])
const emit = defineEmits(['update'])
const handleClick = () => {
emit('update', newValue)
}
八、全局API变化
1、应用实例创建
// Vue2
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
// Vue3
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
2、全局配置
// Vue2
Vue.config.productionTip = false
Vue.use(VueRouter)
Vue.component('MyComponent', MyComponent)
// Vue3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(VueRouter)
app.component('MyComponent', MyComponent)
app.mount('#app')
九、 状态管理
1、Vuex (Vue2/Vue3)
// Vuex 4 示例
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0,
user: null
},
mutations: {
SET_COUNT(state, count) {
state.count = count
},
SET_USER(state, user) {
state.user = user
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.getUser()
commit('SET_USER', user)
}
},
getters: {
doubleCount: state => state.count * 2
}
})
// 在组件中使用
export default {
computed: {
...mapState(['count', 'user']),
...mapGetters(['doubleCount'])
},
methods: {
...mapActions(['fetchUser']),
increment() {
this.$store.commit('SET_COUNT', this.count + 1)
}
}
}
2、Pinia (Vue3推荐)
// Pinia 示例
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: null
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchUser() {
this.user = await api.getUser()
}
}
})
// 在组件中使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const store = useCounterStore()
return {
count: store.count,
doubleCount: store.doubleCount,
increment: store.increment,
fetchUser: store.fetchUser
}
}
}
3、vuex和pinia主要区别
特性 | Vuex | Pinia |
---|---|---|
Mutations | 必须通过mutations修改state | 可以直接在actions中修改state,或直接修改 |
TypeScript支持 | 需要额外配置,支持度一般 | 一流的TypeScript支持,完全类型安全 |
模块化 | 需要namespaced模块,较复杂 | 天然模块化,每个store都是独立模块 |
代码组织 | 需要区分mutations/actions | 更简洁,减少模板代码 |
Composition API | 需要额外适配 | 专为Composition API设计 |
包大小 | 较大(约4kB) | 更轻量(约1kB) |
开发体验 | 需要遵循严格模式 | 更灵活,减少约束 |
DevTools支持 | 完善 | 同样完善 |
十、 构建工具对比
1、Webpack (Vue2常用)
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
devServer: {
hot: true,
port: 8080
}
}
2、Vite (Vue3推荐)
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
hot: true
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router']
}
}
}
}
})
3、webpack和vite主要区别
1、核心架构差异
webpack
基于打包器架构:在开发环境下需要先打包所有代码才能启动开发服务器
使用JavaScript作为通用语言:所有文件都需要经过转换和打包
基于插件系统:功能扩展依赖于庞大的插件生态系统
vite
基于原生ES模块:利用浏览器原生支持ES模块的能力
按需编译:只编译当前页面需要的模块,无需打包整个应用
分为开发和生产环境:开发环境使用ES模块,生产环境使用Rollup打包
2、开发服务器启动速度
webpack:随着项目规模增大,启动时间线性增长(可能需要数十秒甚至几分钟)
vte:启动时间几乎与项目规模无关(通常只需几毫秒到几秒)
3、热更新速度(HMR)
webpack:需要重新构建修改的模块及其依赖关系
vite:仅需编译单个文件,HMR更新速度极快(通常<50ms)
4、构建速度
webpack:需要完整打包所有资源
vite:生产环境使用Rollup进行构建,通常比Webpack更快
5、配置复杂度
webpack
配置复杂:需要详细配置loader、plugin、优化选项等
学习曲线陡峭:需要理解各种概念如chunk分割、tree shaking等
灵活性高:几乎可以通过配置实现任何构建需求
vite
开箱即用:默认支持TypeScript、JSX、CSS等,无需复杂配置
配置简单:大多数项目只需极简配置
约定优于配置:提供合理的默认值,减少配置负担
特性 |
Webpack | Vite |
---|---|---|
开发服务器启动 | 慢(需完整打包) | 极快(按需编译) |
热更新 | 较慢(重建依赖图) | 极快(单个文件编译) |
配置复杂度 | 高 | 低 |
TypeScript支持 | 需要ts-loader | 原生支持 |
CSS处理 | 需要css-loader等 | 原生支持 |
框架支持 | 通用(需配置) | Vue/React优先 |
生产构建 | Webpack自身 | 使用Rollup |
生态插件 | 极其丰富 | 正在增长 |
十一、 API风格对比
1、Options API (Vue2/Vue3)
代码基于选项的组织方式:将代码按照功能类型分组到不同的选项中(data、methods、computed等)
关注点分离:相同功能的代码可能分散在不同的选项中,大型组件难以维护和理解
// Options API 示例
export default {
name: 'UserList',
props: {
users: {
type: Array,
default: () => []
}
},
data() {
return {
searchQuery: '',
selectedUser: null,
loading: false
}
},
computed: {
filteredUsers() {
return this.users.filter(user =>
user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
)
}
},
watch: {
searchQuery(newVal, oldVal) {
console.log('搜索词变化:', oldVal, '->', newVal)
}
},
methods: {
async fetchUsers() {
this.loading = true
try {
const users = await api.getUsers()
this.$emit('update:users', users)
} catch (error) {
console.error('获取用户失败:', error)
} finally {
this.loading = false
}
},
selectUser(user) {
this.selectedUser = user
this.$emit('user-selected', user)
}
},
mounted() {
this.fetchUsers()
}
}
2、Composition API (Vue3)
基于功能的组织方式:将相关功能的代码组织在一起
逻辑关注点集中:相同功能的代码集中在一个地方
// Composition API 示例
import { ref, computed, watch, onMounted } from 'vue'
export default {
name: 'UserList',
props: {
users: {
type: Array,
default: () => []
}
},
emits: ['update:users', 'user-selected'],
setup(props, { emit }) {
// 响应式状态
const searchQuery = ref('')
const selectedUser = ref(null)
const loading = ref(false)
// 计算属性
const filteredUsers = computed(() => {
return props.users.filter(user =>
user.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
// 监听器
watch(searchQuery, (newVal, oldVal) => {
console.log('搜索词变化:', oldVal, '->', newVal)
})
// 方法
const fetchUsers = async () => {
loading.value = true
try {
const users = await api.getUsers()
emit('update:users', users)
} catch (error) {
console.error('获取用户失败:', error)
} finally {
loading.value = false
}
}
const selectUser = (user) => {
selectedUser.value = user
emit('user-selected', user)
}
// 生命周期
onMounted(() => {
fetchUsers()
})
return {
searchQuery,
selectedUser,
loading,
filteredUsers,
fetchUsers,
selectUser
}
}
}
3、<script setup>
语法糖
Vue 3.2 引入了更简洁的 <script setup>
语法,直接将setup加到script标签
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('Component mounted')
})
</script>
4、Options API 和Composition API主要区别
特性 | Options API | Composition API |
---|---|---|
代码组织 | 按选项类型分组 | 按逻辑功能分组 |
逻辑复用 | Mixins(有命名冲突问题) | 组合函数(更好的封装和复用) |
TypeScript支持 | 需要额外努力 | 一流的TypeScript支持 |
学习曲线 | 相对平缓,概念简单 | 需要理解响应式API概念 |
灵活性 | 相对固定 | 极高的灵活性 |
代码可读性 | 简单组件中更清晰 | 复杂组件中更清晰 |
this使用 | 大量使用this | 几乎不需要使用this |
规模适应性 | 适合中小型组件 | 特别适合大型复杂组件 |
十二、 路由系统对比
1、Vue Router 3 (Vue2)
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
// 在组件中使用
export default {
methods: {
goToAbout() {
this.$router.push('/about')
}
},
computed: {
currentRoute() {
return this.$route.name
}
}
}
4、Vue Router 4 (Vue3)
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
// 在Composition API中使用
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
const goToAbout = () => {
router.push('/about')
}
const currentRoute = computed(() => route.name)
return {
goToAbout,
currentRoute
}
}
}
十三、Vue2 Mixins与Vue3 Hooks
1、Vue 2 Mixins
代码混入机制:将mixin对象的内容合并到组件中
选项式合并:相同选项(data、methods等)会进行特定策略的合并
命名冲突风险:多个mixin之间或与组件之间容易产生命名冲突
关系不清晰:难以追踪属性/方法的来源
// mixin.js
export const userMixin = {
data() {
return {
users: [],
loading: false
}
},
methods: {
async fetchUsers() {
this.loading = true
this.users = await fetch('/api/users')
this.loading = false
}
},
mounted() {
this.fetchUsers()
}
}
// 组件中使用
import { userMixin } from './mixins/userMixin'
export default {
mixins: [userMixin],
data() {
return {
// 可能与mixin中的data产生冲突
loading: true // 命名冲突!
}
}
}
2、Vue3 Hooks
函数式组合:通过函数调用明确地组合逻辑
明确的数据来源:每个hook返回的数据来源清晰
命名空间隔离:hook内部的变量名不会与组件或其他hook冲突
按需使用:可以选择性使用hook返回的内容
// useUsers.js
import { ref, onMounted } from 'vue'
export function useUsers() {
const users = ref([])
const loading = ref(false)
const fetchUsers = async () => {
loading.value = true
users.value = await fetch('/api/users')
loading.value = false
}
onMounted(fetchUsers)
return {
users,
loading,
fetchUsers
}
}
// 组件中使用
import { useUsers } from '@/composables/useUsers'
export default {
setup() {
const { users, loading, fetchUsers } = useUsers()
// 可以重命名以避免冲突
const { users: adminUsers } = useAdminUsers()
return {
users,
loading,
fetchUsers
}
}
}
3、Mixins和hook主要区别
特性 | Vue 2 Mixins | Vue 3 Composition Hooks |
---|---|---|
命名冲突 | 容易发生,难以调试 | 不会发生,可以重命名 |
代码来源 | 不透明,难以追踪 | 明确,易于追踪 |
类型支持 | TypeScript支持有限 | 优秀的TypeScript支持 |
逻辑复用 | 选项式混合,不够灵活 | 函数式组合,非常灵活 |
代码组织 | 逻辑分散在不同选项中 | 相关逻辑集中在一起 |
性能影响 | 所有选项都会被合并 | 按需使用,没有额外合并开销 |
可调试性 | 困难,难以追踪问题来源 | 容易,清晰的调用栈 |
参数传递 | 难以向mixin传递参数 | 可以接受参数,高度可配置 |
十四、Teleport组件
Teleport 是 Vue3 提供的一个内置组件,允许我们将组件模板的一部分 "传送" 到 DOM 树的其他位置,而不受父组件 CSS 样式、定位等影响。
to 属性 (必需):指定目标容器,可以是 CSS 选择器或 DOM 元素
禁用功能:可以使用
:disabled
属性动态控制是否启用传送多个 Teleport 到同一目标:多个 Teleport 可以挂载到同一个目标元素,按顺序追加
<!-- <div class="modal"> 会被渲染到 <body> 元素的末尾,而不是在其父组件的 DOM 位置。-->
<template>
<div class="app">
<h1>主应用</h1>
<!-- 将内容传送到 body 元素下 -->
<Teleport to="body">
<div class="modal">
这是一个模态框
</div>
</Teleport>
</div>
</template>
十五、 TypeScript支持
1、 类型推断
// Vue3 - 更好的TypeScript支持
import { ref, computed } from 'vue'
interface User {
id: number
name: string
}
export default {
setup() {
const user = ref<User>({ id: 1, name: 'John' })
const userName = computed(() => user.value.name)
return { user, userName }
}
}
2、类型安全的props和emits
// Vue3
interface Props {
title: string
count?: number
}
interface Emits {
(e: 'update', value: string): void
(e: 'delete'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
十六、总结
特性 | Vue2 | Vue3 |
---|---|---|
API 设计 | Options API | Composition API |
状态管理 | Vuex | Pinia |
构建工具 | Webpack | Vite |
响应式系统 | Object.defineProperty | Proxy |
TypeScript 支持 | 有限 | 完全支持 |
性能表现 | 一般 | 提升 30-50% |
包体积 | 较大 | 更小 |
开发体验 | 基础 | 优秀 |
学习成本 | 低 | 中等 |
维护性 | 一般 | 更好 |