vue2和vue3的区别
watch
vue2
简单写法
watch: {数据属性名 (newValue, oldValue) {一些业务逻辑 或 异步操作。 },'对象.属性名' (newValue, oldValue) {一些业务逻辑 或 异步操作。 }
}
完整写法
watch: {// watch 完整写法数据属性名: {deep: true, // 深度监视(针对复杂类型)immediate: true, // 是否立刻执行一次handlerhandler (newValue) {console.log(newValue)}}
}
vue3
<script setup>// 1. 导入watchimport { ref, watch } from 'vue'const count = ref(0)// 2. 调用watch 侦听变化watch(count, (newValue, oldValue)=>{console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)})
</script><script setup>// 1. 导入watchimport { ref, watch } from 'vue'const count = ref(0)const name = ref('cp')// 2. 调用watch 侦听变化watch([count, name], ([newCount, newName],[oldCount,oldName])=>{console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName])},{immediate: true},{deep:true})
</script>
生命周期
provide&inject
vue2
父组件provide提供数据
export default {provide () {return {// 普通类型【非响应式】color: this.color, // 复杂类型【响应式】userInfo: this.userInfo, }}
}
子/孙组件inject获取数据
export default {inject: ['color','userInfo'],created () {console.log(this.color, this.userInfo)}
}
vue3
父组件
<script setup>
import { provide, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';// 定义一个响应式数据
const message = ref('Hello from parent');// 使用 provide 提供数据
provide('parentMessage', message);
</script>
后代组件
<script setup>
import { inject } from 'vue';// 使用 inject 获取父组件提供的数据
const receivedMessage = inject('parentMessage');
</script>
模版引用
概念:通过 ref标识 获取真实的 dom对象或者组件实例对象
vue2
利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
1.给要获取的盒子添加ref属性
<div ref="chartRef">我是渲染图表的容器</div>2.获取时通过 $refs获取 this.$refs.chartRef 获取
mounted () {console.log(this.$refs.chartRef)
}
vue3
实现步骤:
- 调用ref函数生成一个ref对象
- 在组件上通过ref标识绑定ref对象
<template><!-- 2. 通过 ref 标识绑定 ref 对象到 h1 标签 --><h1 ref="h1Ref">我是一个 h1 标签</h1><button @click="logDomInfo">点击查看 DOM 信息</button>
</template><script setup>
import { ref } from 'vue';// 1. 调用 ref 函数生成一个 ref 对象,初始值为 null
const h1Ref = ref(null);const logDomInfo = () => {// 当 DOM 渲染完成后,h1Ref.value 就是对应的真实 h1 DOM 元素console.log('h1 标签的内容:', h1Ref.value.innerHTML);console.log('h1 标签的类名:', h1Ref.value.className);// 还可以对 DOM 进行操作,比如修改内容// h1Ref.value.innerHTML = '内容被修改啦';
};
</script>
自定义指令
vue2
// 全局注册一个自动聚焦指令 v-focus
Vue.directive('focus', {// 当被绑定的元素插入到DOM中时触发// 被绑定元素插入父节点时调用(此时元素已出现在 DOM 中)。inserted(el) {// el是指令所绑定的DOM元素el.focus() // 自动聚焦}
})// 全局注册一个设置颜色的指令 v-color
Vue.directive('color', {// 指令第一次绑定到元素时调用(只执行一次)。// 通常用于初始化操作,如设置初始样式、事件监听等。bind(el, binding) {// binding.value 是指令的绑定值el.style.color = binding.value}
})// 局部注册自定义指令和data、methods平级directives: {// 局部指令v-border:给元素添加边框border: {// 当元素被插入DOM时执行inserted(el, binding) {el.style.border = `${binding.value}px solid #333`el.style.padding = '10px'el.style.marginTop = '10px'},// 当绑定值更新时执行update(el, binding) {el.style.borderWidth = `${binding.value}px`}}},
vue3
<template><div><input v-focus /></div>
</template><script setup>
import { defineDirective } from 'vue';// 定义自定义指令
const focus = defineDirective({// 当绑定元素插入到 DOM 中时调用mounted(el) {el.focus();}
});
</script>
除了 mounted
钩子外,自定义指令还提供了 beforeMount
、updated
、beforeUpdate
、unmounted
等钩子,以便在不同的阶段对绑定元素进行操作。
状态管理工具
vuex
src/
├── components/
│ ├── CartDemo.vue
│ ├── Son1.vue
│ └── Son2.vue
├── store/
│ ├── modules/
│ │ ├── cart.js
│ │ ├── setting.js
│ │ └── user.js
│ └── index.js
├── App.vue
└── main.js
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import axios from 'axios'// 全局配置axios
Vue.prototype.$axios = axiosVue.config.productionTip = falsenew Vue({store, // 挂载storerender: h => h(App)
}).$mount('#app')
App.vue
<template><div id="app"><h1>Vuex 完整示例</h1><p>根状态计数: {{ $store.state.rootCount }} (翻倍: {{ $store.getters.rootCountDouble }})</p><button @click="$store.commit('incrementRoot')">根计数+1</button><button @click="$store.dispatch('asyncIncrementRoot')">1秒后根计数+1</button><hr><Son1 /><hr><Son2 /><hr><CartDemo /></div>
</template><script>
import Son1 from './components/Son1.vue'
import Son2 from './components/Son2.vue'
import CartDemo from './components/CartDemo.vue'export default {name: 'App',components: { Son1, Son2, CartDemo },created() {// 初始化购物车数据this.$store.dispatch('cart/getCartList')}
}
</script><style>
#app {max-width: 1000px;margin: 0 auto;padding: 20px;
}
button {margin: 5px;padding: 6px 12px;cursor: pointer;
}
hr {margin: 20px 0;
}
</style>
index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import setting from './modules/setting'
import cart from './modules/cart'// 安装Vuex插件
Vue.use(Vuex)// 创建并导出store实例
export default new Vuex.Store({// 开启严格模式(生产环境建议关闭)strict: process.env.NODE_ENV !== 'production',// 根级别状态(简单示例)state: {rootCount: 0},// 根级别mutationsmutations: {incrementRoot(state) {state.rootCount++}},// 根级别actionsactions: {asyncIncrementRoot(context) {setTimeout(() => {context.commit('incrementRoot')}, 1000)}},// 根级别gettersgetters: {rootCountDouble(state) {return state.rootCount * 2}},// 注册模块modules: {user,setting,cart}
})
cart.js
import axios from 'axios'// 购物车模块
const namespaced = trueconst state = {list: []
}const mutations = {// 更新购物车列表updateList(state, payload) {state.list = payload},// 更新商品数量updateCount(state, { id, count }) {const item = state.list.find(item => item.id === id)if (item) item.count = count}
}const actions = {// 获取购物车数据async getCartList(context) {try {const res = await axios.get('http://localhost:3000/cart')context.commit('updateList', res.data)} catch (err) {console.error('获取购物车数据失败:', err)}},// 更新商品数量async updateCartCount(context, { id, count }) {try {await axios.patch(`http://localhost:3000/cart/${id}`, { count })context.commit('updateCount', { id, count })} catch (err) {console.error('更新数量失败:', err)}}
}const getters = {// 计算商品总数totalCount(state) {return state.list.reduce((total, item) => total + item.count, 0)},// 计算商品总价totalPrice(state) {return state.list.reduce((total, item) => total + (item.price * item.count), 0)}
}export default {namespaced,state,mutations,actions,getters
}
setting.js
// 设置模块
const namespaced = trueconst state = {theme: 'light',desc: '默认描述信息'
}const mutations = {// 设置主题setTheme(state, newTheme) {state.theme = newTheme},// 设置描述setDesc(state, newDesc) {state.desc = newDesc}
}const actions = {// 异步设置描述setDescAsync(context, newDesc) {setTimeout(() => {context.commit('setDesc', newDesc)}, 1500)}
}const getters = {// 带前缀的主题名prefixedTheme(state) {return `主题:${state.theme}`}
}export default {namespaced,state,mutations,actions,getters
}
user.js
// 用户模块
const namespaced = trueconst state = {userInfo: {name: '张三',age: 25},myMsg: '用户模块默认消息'
}const mutations = {// 更新用户信息setUser(state, newUserInfo) {state.userInfo = newUserInfo},// 更新消息updateMsg(state, msg) {state.myMsg = msg}
}const actions = {// 异步更新用户信息setUserAsync(context, newUserInfo) {setTimeout(() => {context.commit('setUser', newUserInfo)}, 1000)}
}const getters = {// 用户名转为大写upperCaseName(state) {return state.userInfo.name.toUpperCase()},// 用户信息描述userDescription(state) {return `姓名:${state.userInfo.name},年龄:${state.userInfo.age}`}
}export default {namespaced,state,mutations,actions,getters
}
CartDemo.vue
<template><div class="cart-demo"><h2>购物车案例</h2><!-- 商品列表 --><div class="cart-list"><div v-for="item in cartList" :key="item.id" class="cart-item"><img :src="item.thumb" alt="商品图片" class="item-img"><div class="item-info"><h4>{{ item.name }}</h4><p class="price">¥{{ item.price }}</p><div class="count-control"><button @click="changeCount(item.id, -1)">-</button><span>{{ item.count }}</span><button @click="changeCount(item.id, 1)">+</button></div></div></div></div><!-- 合计信息 --><div class="cart-summary"><p>共 {{ totalCount }} 件商品</p><p>合计: ¥{{ totalPrice.toFixed(2) }}</p><button class="checkout-btn">结算</button></div></div>
</template><script>
import { mapState, mapGetters } from 'vuex'export default {name: 'CartDemo',computed: {...mapState('cart', ['list']),...mapGetters('cart', ['totalCount', 'totalPrice']),cartList() {return this.list}},methods: {changeCount(id, step) {const item = this.cartList.find(item => item.id === id)if (!item) returnconst newCount = item.count + stepif (newCount < 1) returnthis.$store.dispatch('cart/updateCartCount', {id,count: newCount})}}
}
</script><style scoped>
.cart-demo {padding: 15px;border: 1px solid #999;
}
.cart-list {margin-bottom: 20px;
}
.cart-item {display: flex;margin: 10px 0;padding: 10px;border: 1px solid #eee;
}
.item-img {width: 80px;height: 80px;object-fit: cover;margin-right: 15px;
}
.item-info {flex: 1;
}
.price {color: #f00;font-weight: bold;
}
.count-control {display: flex;align-items: center;margin-top: 10px;
}
.count-control button {width: 30px;height: 30px;display: flex;align-items: center;justify-content: center;
}
.count-control span {margin: 0 10px;width: 30px;text-align: center;
}
.cart-summary {text-align: right;padding: 10px;border-top: 2px solid #ccc;
}
.checkout-btn {background: #42b983;color: white;border: none;padding: 8px 20px;margin-top: 10px;
}
</style>
Son1.vue
<template><div class="son1"><h2>Son1 组件(基础用法)</h2><!-- 访问state --><div><h3>用户信息</h3><p>用户名: {{ $store.state.user.userInfo.name }}</p><p>年龄: {{ $store.state.user.userInfo.age }}</p><p>用户消息: {{ userMsg }}</p></div><!-- 访问getters --><div><h3>处理后信息</h3><p>大写用户名: {{ $store.getters['user/upperCaseName'] }}</p><p>用户描述: {{ userDesc }}</p><p>主题: {{ $store.getters['setting/prefixedTheme'] }}</p></div><!-- 调用mutations --><div><h3>修改数据</h3><button @click="updateUserName">修改用户名</button><button @click="changeTheme">切换主题</button></div></div>
</template><script>
import { mapState, mapGetters } from 'vuex'export default {name: 'Son1',computed: {// 映射user模块的state...mapState('user', {userMsg: 'myMsg'}),// 映射user模块的getters...mapGetters('user', ['userDescription']),// 重命名映射userDesc() {return this.userDescription}},methods: {updateUserName() {this.$store.commit('user/setUser', {name: '李四',age: 30})},changeTheme() {const newTheme = this.$store.state.setting.theme === 'light' ? 'dark' : 'light'this.$store.commit('setting/setTheme', newTheme)}}
}
</script><style scoped>
.son1 {padding: 15px;border: 1px solid #ccc;
}
</style>
Son2.vue
<template><div class="son2"><h2>Son2 组件(辅助函数用法)</h2><!-- 使用映射的state --><div><p>用户消息: {{ myMsg }}</p><p>当前主题: {{ theme }}</p><p>描述信息: {{ desc }}</p></div><!-- 使用映射的getters --><div><p>大写用户名: {{ upperCaseName }}</p><p>主题信息: {{ prefixedTheme }}</p></div><!-- 使用映射的mutations和actions --><div><button @click="updateMsg('通过mapMutations修改的消息')">修改用户消息</button><button @click="setDesc('通过mapActions修改的描述')">修改描述</button><button @click="setUserAsync({ name: '王五', age: 28 })">异步修改用户</button></div></div>
</template><script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'export default {name: 'Son2',computed: {// 映射多个模块的state...mapState('user', ['myMsg', 'userInfo']),...mapState('setting', ['theme', 'desc']),// 映射多个模块的getters...mapGetters('user', ['upperCaseName']),...mapGetters('setting', ['prefixedTheme'])},methods: {// 映射mutations...mapMutations('user', ['updateMsg']),...mapMutations('setting', ['setTheme']),// 映射actions...mapActions('user', ['setUserAsync']),...mapActions('setting', ['setDescAsync']),// 自定义方法setDesc(newDesc) {this.setDescAsync(newDesc)}}
}
</script><style scoped>
.son2 {padding: 15px;border: 1px solid #666;
}
</style>
pinia
src/
├── router/
│ └── index.js
├── store/
│ ├── counterStore.js
│ └── listStore.js
├── views/
│ ├── CounterView.vue
│ └── ListView.vue
├── App.vue
└── main.js
main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
import './assets/main.css'// 创建Pinia实例
const pinia = createPinia()
// 使用持久化插件
pinia.use(persist)// 创建应用并挂载
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
App.vue
<template><div id="app"><h1>Pinia 状态管理示例</h1><nav><router-link to="/counter">计数器示例</router-link> |<router-link to="/list">列表示例</router-link></nav><router-view /></div>
</template><script setup>
// 主组件仅负责路由导航和布局
</script><style>
#app {max-width: 1200px;margin: 0 auto;padding: 20px;font-family: Arial, sans-serif;
}nav {margin: 20px 0;padding: 10px;background-color: #f5f5f5;border-radius: 4px;
}router-link {margin-right: 15px;color: #42b983;text-decoration: none;
}router-link.active {font-weight: bold;color: #359e75;
}
</style>
ListView.vue
<template><div class="list-view"><h2>待办事项与频道列表</h2><!-- 待办事项部分 --><div class="todo-section"><h3>待办事项</h3><div class="todo-input"><input v-model="newTodoTitle" placeholder="输入新的待办事项"@keyup.enter="addTodo"><button @click="addTodo">添加</button></div><div class="todo-list"><div v-for="todo in todos" :key="todo.id"class="todo-item":class="{ completed: todo.completed }"><span @click="toggleTodo(todo.id)">{{ todo.title }}</span><button @click="deleteTodo(todo.id)">删除</button></div></div><div class="todo-stats"><p>统计: 共 {{ todoStats.total }} 项, 已完成 {{ todoStats.completed }} 项, 未完成 {{ todoStats.uncompleted }} 项</p><p>完成率: {{ todoStats.completionRate }}%</p></div></div><!-- 频道列表部分 --><div class="channel-section"><h3>频道列表</h3><button @click="fetchChannels" :disabled="loading">{{ loading ? '加载中...' : '获取频道列表' }}</button><div class="channel-list" v-if="channelList.length"><div v-for="channel in channelList" :key="channel.id" class="channel-item">{{ channel.name }}</div></div><p v-if="!loading && channelList.length === 0 && fetched">暂无频道数据</p></div></div>
</template><script setup>
import { useListStore } from '../store/listStore';
import { storeToRefs } from 'pinia';
import { ref, onMounted } from 'vue';// 获取store实例
const listStore = useListStore();// 使用storeToRefs解构并保持响应式
const { todos, channelList, loading,todoStats
} = storeToRefs(listStore);// 新待办事项标题
const newTodoTitle = ref('');
// 是否已经获取过频道数据
const fetched = ref(false);// 解构方法
const { addTodo, toggleTodo, deleteTodo, fetchChannels } = listStore;// 自定义添加待办事项方法
const handleAddTodo = () => {addTodo(newTodoTitle.value);newTodoTitle.value = '';
};// 页面挂载时获取频道列表
onMounted(async () => {await fetchChannels();fetched.value = true;
});
</script><style scoped>
.list-view {padding: 20px;border: 1px solid #ccc;border-radius: 6px;margin: 10px;
}.todo-section, .channel-section {margin-bottom: 30px;padding-bottom: 20px;border-bottom: 1px dashed #ccc;
}.todo-input {margin: 15px 0;display: flex;gap: 10px;
}.todo-input input {flex: 1;padding: 8px;
}.todo-list {margin: 15px 0;
}.todo-item {display: flex;justify-content: space-between;align-items: center;padding: 8px;margin: 5px 0;background-color: #f9f9f9;border-radius: 4px;
}.todo-item.completed span {text-decoration: line-through;color: #999;
}.todo-stats {margin-top: 15px;padding: 10px;background-color: #f0f0f0;border-radius: 4px;
}.channel-list {margin: 15px 0;display: flex;flex-wrap: wrap;gap: 10px;
}.channel-item {padding: 8px 16px;background-color: #f0f0f0;border-radius: 16px;
}button {padding: 8px 16px;cursor: pointer;background-color: #42b983;color: white;border: none;border-radius: 4px;
}button:disabled {background-color: #999;cursor: not-allowed;
}
</style>
CounterView.vue
<template><div class="counter-view"><h2>计数器示例</h2><div class="info"><p>当前计数: {{ counterStore.count }}</p><p>计数翻倍: {{ counterStore.doubleCount }}</p><p>计数翻倍+1: {{ counterStore.doubleCountPlusOne }}</p><p>{{ counterStore.getUserAgeDesc('用户年龄:') }}</p><p>用户名: {{ userInfo.username }}</p></div><div class="actions"><button @click="counterStore.increment()">+1</button><button @click="counterStore.increment(5)">+5</button><button @click="counterStore.decrement()">-1</button><button @click="counterStore.decrement(5)">-5</button><button @click="fetchUserInfo" :disabled="loading">{{ loading ? '加载中...' : '获取用户信息' }}</button></div></div>
</template><script setup>
import { useCounterStore } from '../store/counterStore';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';// 获取store实例
const counterStore = useCounterStore();// 使用storeToRefs保持响应式
const { userInfo } = storeToRefs(counterStore);// 加载状态
const loading = ref(false);// 调用异步action
const fetchUserInfo = async () => {loading.value = true;try {const info = await counterStore.fetchUserInfo();console.log('获取用户信息成功:', info);} catch (error) {console.error('获取用户信息失败:', error);} finally {loading.value = false;}
};
</script><style scoped>
.counter-view {padding: 20px;border: 1px solid #ccc;border-radius: 6px;margin: 10px;
}.info {margin: 20px 0;padding: 10px;background-color: #f5f5f5;border-radius: 4px;
}.actions {display: flex;gap: 10px;flex-wrap: wrap;
}button {padding: 8px 16px;cursor: pointer;background-color: #42b983;color: white;border: none;border-radius: 4px;
}button:hover {background-color: #359e75;
}button:disabled {background-color: #999;cursor: not-allowed;
}
</style>
counterStore.js
import { defineStore } from "pinia";// 定义并导出计数器store
export const useCounterStore = defineStore("counter", {// 状态state() {return {count: 100,name: "计数器",userInfo: {username: "张三",age: 20}}},// 计算属性(类似Vuex的getters)getters: {// 基础用法doubleCount(state) {return state.count * 2;},// 使用this访问其他gettersdoubleCountPlusOne() {return this.doubleCount + 1;},// 带参数的getter(返回函数)getUserAgeDesc() {return (prefix) => `${prefix}${this.userInfo.age}岁`;}},// 方法(同步和异步操作)actions: {// 同步方法increment(step = 1) {this.count += step;},decrement(step = 1) {this.count -= step;},// 异步方法async fetchUserInfo() {// 模拟API请求await new Promise(resolve => setTimeout(resolve, 1000));// 模拟请求结果this.userInfo = {username: "李四",age: 25};return this.userInfo;}},// 持久化配置persist: {// 指定需要持久化的字段paths: ['count', 'userInfo'],// 存储的键名key: 'counter_store',// 存储方式:localStorage(默认)或sessionStoragestorage: localStorage}
});
listStore.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import axios from "axios";// 定义并导出列表store
export const useListStore = defineStore("list", () => {// 状态(相当于state)const todos = ref([{ id: 1, title: "学习Pinia", completed: false },{ id: 2, title: "掌握组合式API", completed: false }]);const channelList = ref([]);const loading = ref(false);// 计算属性(相当于getters)const completedCount = computed(() => {return todos.value.filter(item => item.completed).length;});const uncompletedCount = computed(() => {return todos.value.length - completedCount.value;});const todoStats = computed(() => {return {total: todos.value.length,completed: completedCount.value,uncompleted: uncompletedCount.value,completionRate: todos.value.length ? Math.round((completedCount.value / todos.value.length) * 100) : 0};});// 方法(相当于actions)const addTodo = (title) => {if (!title.trim()) return;todos.value.push({id: Date.now(),title,completed: false});};const toggleTodo = (id) => {const todo = todos.value.find(item => item.id === id);if (todo) {todo.completed = !todo.completed;}};const deleteTodo = (id) => {todos.value = todos.value.filter(item => item.id !== id);};// 异步方法 - 获取频道列表const fetchChannels = async () => {try {loading.value = true;const response = await axios.get("http://geek.itheima.net/v1_0/channels");channelList.value = response.data.data.channels;return channelList.value;} catch (error) {console.error("获取频道列表失败:", error);return [];} finally {loading.value = false;}};// 返回需要暴露的状态、计算属性和方法return {todos,channelList,loading,completedCount,uncompletedCount,todoStats,addTodo,toggleTodo,deleteTodo,fetchChannels};
}, {// 持久化配置persist: {paths: ['todos'], // 只持久化todosstorage: sessionStorage // 使用sessionStorage}
});
index.js
import { createRouter, createWebHistory } from 'vue-router'
import CounterView from '../views/CounterView.vue'
import ListView from '../views/ListView.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',redirect: '/counter'},{path: '/counter',name: 'counter',component: CounterView},{path: '/list',name: 'list',component: ListView}]
})export default router