文章目录
vue3
一、初识
技术栈官网
技术栈:Vue3+VueRouter4+Pinia2+Vant3/ElementPlus
1、Vue3官网:https://vuejs.org/
2、VueRouter(V4):https://router.vuejs.org/
3、Pinia(V2):https://pinia.vuejs.org/
4、Vite构建工具:https://vitejs.dev/
5、ElementPlus:https://element-plus.gitee.io/zh-CN/
6、Vant(v3):https://vant-contrib.gitee.io/vant/#/zh-CN
搭建环境
- @vue/cli
- vite(推荐)
使用vite搭建项目
第一步:yarn create vite vue3-admin --template vue-ts
第二步:cd vue3-admin
第三步:yarn
第四步:yarn dev
目录分析
- vite.config.js 是Vite官方配置文件,各种配置参考Vite官网。
- tsconfig.json 是TypeScript的配置文件
- index.html 是SPA挂载的根页面
- xxx.d.ts 是TypeScript的声明文件
- main.ts 是入口文件
- App.vue 是根组件(SFC单文件组织)
在Vue3中关于SFC的若干变化
- 支持多个style标签、支持多个script标签、template支持多个根节点
- 在script标签中通过设置lang='ts’表示是否启用ts
- 在style标签中,可以使用 v-bind指令。
四种语法范式
- 选项式写法:完成支持Vue2写法,我们之前学习的Vue2的写法就是选项式写法
- 组合式写法:只使用setup(),把组合式API写在setup中就是组合式写法
- 选项式+组合式写法:可以使用setup()+选项式写法
- 组合式语法糖写法:在script标签上添加setup,只支持组合式写法,规避选项式写法。
选项式写法
// Demo.vue
<template>
<h1>选项式API写法</h1>
<h1 v-text="num"></h1>
<button @click="add">+1</button>
</template>
<script>
export default {
data() {
return {
num: 0,
};
},
methods: {
add() {
this.num++;
},
},
};
</script>
// App.vue
<template><Demo /></template>
<script>
import Demo from "./components/Demo.vue";
export default {
components: {
Demo,
},
};
</script>
<style scoped>
</style>
组合式写法
// Demo.vue
<template>
<h1>组合式API写法</h1>
<h1 v-text="num"></h1>
<button @click="add">+1</button>
</template>
<script>
import { ref } from "vue";
// Vue3的选项写法:对Vue2范式完全兼容,可以同时使用setup和选项,也可只使用setup。
// 官方已经不推荐使用这种选项写法了。选项写法,都有对应的组合API来实现。
// 解读setup这个选项:相当于Vue2中的created(),可以理解成是组件生命周期的第一阶段;setup(props,context),context表示上下文,为什么在setup中要有一个上下文对象,因为在setup选项中没有this。
export default {
// 组合选项
setup(props, context) {
const num = ref(1);
const add = () => {
num.value++;
};
return {
num,
add,
};
},
};
</script>
选项式+组合式写法
// Demo.vue
<template>
<h1>选项式+组合式API写法</h1>
<h1 v-text="num"></h1>
<button @click="add">+1</button>
</template>
<script>
import { ref } from "vue";
// 个人非常不推荐这种写法
export default {
props: {
xx: { type: String, default: "" },
},
// 组合选项
setup(props, context) {
const num = ref(1);
return {
num,
};
},
methods: {
add() {
this.num++;
},
},
};
</script>
组合式语法糖写法
<template>
<h1>组合式语法糖写法</h1>
<h1 v-text="num"></h1>
<button @click="add">+1</button>
</template>
<script setup>
// 在Vue中,怎么规避选项式写法?在<script setup>,这种在script上添加setup是Vue3组合式写法的语法糖。并且是官方推荐的语法糖
// 个人非常推荐
import { ref } from "vue";
const num = ref(10);
const add = () => {
num.value++;
};
</script>
hooks
把逻辑进行封装,封装成一个个的hooks
// Demo.vue
<template>
<h1 v-text="num"></h1>
<button @click="sub">自减</button>
<button @click="add">自增</button>
<hr />
<input type="text" v-model="task" @keyup.enter="confirm" />
<div v-for="(item, idx) in list" :key="item.id">
<span v-text="item.id"></span>
<span v-text="item.task"></span>
<button @click="delTodo(idx)">删除</button>
</div>
</template>
<script setup>
import useCounter from "./hooks/useCounter.ts";
import useTodolist from "./hooks/useTodolist.ts";
const { num, add, sub } = useCounter();
const { task, list, confirm, delTodo } = useTodolist();
</script>
// useCounter.ts
import {
ref
} from 'vue'
function useCounter() {
const num = ref(100)
const add = () => {
num.value += 10
}
const sub = () => {
num.value -= 5
}
return {
num,
add,
sub
}
}
export default useCounter
// useTodolist
import {
ref,
reactive
} from 'vue'
function useTodolist() {
let task = ref('')
let list = reactive([])
let confirm = () => {
if (task.value) {
list.push({
id: Date.now(),
task: task.value
})
task.value = ''
}
}
const delTodo = (idx) => {
list.splice(idx, 1)
}
return {
task,
list,
confirm,
delTodo
}
}
export default useTodolist
Vue3最重要的特性
- 开发思想的变化:Vue3通过使用组合API,可以方便地封装Hooks,分离组件中的“逻辑关注点”。
- 实现:第一步,用组合API替换掉传统的选项写法;第二步,梳理逻辑关注点封装自定义Hooks。
- 自定义Hooks注意的问题:自定义Hooks一定要以use*开头,自定义Hooks可以被复用,自定义Hooks不要过度与泛滥。
- 组件封装 与 Hooks封装,本质的区别:前者是视图结构的封装,后者是逻辑功能的封装。
- Vue3开源项目:https://github.com/pipipi-pikachu/PPTist
二、组合API
- setup组合
- Vue3 中新增的 setup,目的是为了解决 Vue2 中“数据和业务逻辑不分离”的问题
- 第1步: 用setup组合API 替换 vue2 中的data/computed/watch/methods等选项
- 第2步: 把setup中相关联的功能封装成一个个可独立可维护的hooks
- Vue3 中新增的 setup,目的是为了解决 Vue2 中“数据和业务逻辑不分离”的问题
ref
作用:一般用于定义基本数据类型数据,比如 String / Boolean / Number等
原理:ref 的背后是使用 reactive 来实现的响应式
语法:const x = ref(100)
访问:在 setup 中使用 .value 来访问
// Demo.vue
<template>
<h1>ref的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<button @click="add">自增</button>
</template>
<script setup>
import {ref} from "vue"
// num叫一个ref变量,也叫ref对象
let num = ref(1000);
let add = ()=>{
num.value++
}
</script>
<style lang="less" scoped>
</style>
isRef
作用:判断一个变量是否为一个 ref 对象。
语法:const bol = isRef(x)
// Demo.vue
<template>
<h1>isRef的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<button @click="add">自增</button>
</template>
<script setup>
import {ref,isRef} from "vue"
let num = ref(1000);
let add = ()=>{
num.value++
}
// num就是一个ref变化
console.log(isRef(num)); // true
console.log(isRef(110)); // false
console.log(isRef(num.value)); // false
</script>
<style lang="less" scoped>
</style>
unref
作用:用于返回一个值,如果访问的是 ref变量,就返回其 .value值;如果不是 ref变量,就直接返回。
语法:const x = unref(y)
// Demo.vue
<template>
<h1>unref的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<button @click="add">自增</button>
</template>
<script setup>
import {ref,isRef,unref} from "vue"
let num = ref(1000);
let add = ()=>{
num.value++
}
console.log(isRef(num));
// 如果给unref传递一个ref变量,得到这个ref的value值
console.log(unref(num));
// 如果给unref传递的不是一个ref变量,得到这个值的本身
console.log(unref(num.value));
console.log(unref(110));
</script>
<style lang="less" scoped>
</style>
customRef
作用:自定义ref对象,把ref对象改写成get/set,进一步可以为它们添加 track/trigger。
// Demo.vue
<template>
<h1>customRef的使用</h1>
<input type="text" v-model="name" />
</template>
<script setup>
// import {ref,isRef,unref} from "vue"
// let name = ref("");
import { customRef,onRenderTracked,onRenderTriggered } from "vue"
let name = customRef((track,trigger)=>{
let value = "";
return {
get(){
track(); // 如果有人访问name,就执行track()
return value
},
set(val){
trigger(); // 如果有人修改name,就执行trigger
value = val;
}
}
})
// 仅供在开发环境下,用于ref变量的调试
onRenderTracked((ev) => console.log("name被访问了", ev));
onRenderTriggered((ev) => console.log("name被修改了", ev));
</script>
<style lang="less" scoped>
</style>
toRef
作用:把一个 reactive对象中的某个属性变成 ref 变量。
语法:const x = toRef(reactive(obj), ‘key’) // x.value
// Demo.vue
<template>
<h1>toRef的使用</h1>
<h1 v-text="user.name"></h1>
</template>
<script setup>
import {ref,isRef,unref,reactive,toRef} from "vue"
let user = reactive({name:"malu",age:18});
setTimeout(() => {
user.name = "码路"
}, 1000);
// 能不能把user中的name变成ref变量呢?
// 答:toRef
console.log(user.name);
console.log(isRef(user.name));
console.log("-------");
let name = toRef(user,"name");
console.log(name.value);
console.log(isRef(name));
</script>
<style lang="less" scoped>
</style>
toRefs
作用:把一个reactive响应式对象变成ref变量。
语法:const obj1 = toRefs(reactive(obj))
应用:在子组件中接收父组件传递过来的 props时,使用 toRefs把它变成响应式的。
// Demo.vue
<template>
<h1>toRefs的使用</h1>
<h2 v-text="name"></h2>
<h2 v-text="age"></h2>
</template>
<script setup>
import {ref,isRef,unref,reactive,toRefs} from "vue"
let user = reactive({name:"malu",age:18});
let {name,age} = toRefs(user);
console.log(isRef(name));
console.log(isRef(age));
</script>
<style lang="less" scoped>
</style>
shallowRef
作用:对复杂层级的对象,只将其第一层变成 ref 响应。 (性能优化)
语法:const x = shallowRef({a:{b:{c:1}}, d:2}) 如此a、b、c、d变化都不会自动更新,需要借助 triggerRef 来强制更新。
// Demo.vue
<template>
<h1>shallowRef的使用</h1>
<h1 v-text="info1.a.b.c"></h1>
<button @click="add1">自增</button>
<hr />
<h1 v-text="info2.a.b.c"></h1>
<button @click="add2">自增</button>
</template>
<script setup>
import {shallowRef,ref,triggerRef} from "vue"
// let info1 = ref("wc");
// ref是定义基本数据数据的响应式
// reactive是定义引用数据类型的响应式
// 其实ref也可以定义,如果ref中写了一人引用数据类型,它的内部还是会调用reactive
let info1 = ref({a:{b:{c:3}}});
console.log(info1.value.a.b.c);
let add1 = ()=>{
info1.value.a.b.c++
}
// -------------------
// {a:{b:{c:3}}} 整体替换的话是响应式的,对a,b,c修改并不是响应式的
let info2 = shallowRef({a:{b:{c:3}}});
console.log(info2.value.a.b.c);
let add2 = ()=>{
// info2.value = {a:{b:{c:10000}}}
// info2.value.a = {b:{c:10000}}
// info2.value.a.b = {c:10000}
// info2.value.a.b.c = 10000
info2.value.a.b.c++
triggerRef(info2); // 强制刷新页面
}
</script>
<style lang="less" scoped>
</style>
riggerRef
作用:强制更新一个 shallowRef对象的渲染。
语法:triggerRef(shallowRef对象)
reactive
作用:定义响应式变量,一般用于定义引用数据类型。如果是基本数据类型,建议使用ref来定义。
语法:const info = reactive([] | {})
注意:声明时,写成对象形式,不然不会刷新页面。例如reactive({arr:[]})。(内部原理不??)
// Demo.vue
<template>
<h1>reactive的使用</h1>
<h1 v-text="user.a"></h1>
<button @click="add">+1</button>
<h1 v-text="user.b"></h1>
</template>
<script setup>
import {reactive} from "vue"
let obj = {a:1,b:2,c:3}
let user = reactive(obj);
let add = ()=>{
user.a++
}
</script>
<style lang="less" scoped>
</style>
readonly
作用:把一个对象,变成只读的。
语法:const rs = readonly(ref对象 | reactive对象 | 普通对象)
// Demo.vue
<template>
<h1>readonly的使用</h1>
<h1 v-text="user2.a"></h1>
<button @click="add">+1</button>
</template>
<script setup>
import {reactive,readonly,isReadonly,ref} from "vue"
let obj = {a:1,b:2}
let user = reactive(obj);
// readonly(ref对象 | reactive对象 | 普通对象)
// readonly它内部是拦截的set
let info = readonly({c:3,d:4}); // info叫readonly对象
let user2 = readonly(user)
let foo = ref(100);
let foo2 = readonly(foo)
let xx = 666;
let add = ()=>{
// key "a" failed: target is readonly.
// a是只读,只能使用,不能修改
user2.a++; // 不能
}
console.log(isReadonly(info)); // true
console.log(isReadonly(user2)); // true
console.log(isReadonly(foo2)); // true
console.log("---------");
console.log(isReadonly(foo));
console.log(isReadonly(user));
console.log(isReadonly(xx));
</script>
<style lang="less" scoped>
</style>
isReadonly
作用: 判断一个变量是不是只读的。
语法:const bol = isReadonly(变量)
isReactive
作用:判断一变量是不是 reactive的。
注意:被 readonly代理过的 reactive变量,调用 isReactive 也是返回 true的。
// Demo.vue
<template>
<h1>isReactive的使用</h1>
<h1 v-text="user2.a"></h1>
<button @click="add">+1</button>
</template>
<script setup>
import { reactive, readonly, ref, isReactive } from "vue";
const uu = { a: 1, b: 2 };
const user = reactive(uu);
const info = readonly({ c: 3, d: 4 }); // 是Readonly
const user2 = readonly(user); // 是Readonly
const foo = ref(100); // foo是一个ref,不是reactive
const foo2 = readonly(foo); // 是Readonly
const xx = 100;
const add = () => {
user2.a++;
};
// 如果readonly包了一个reactive,这个readonly也是reactive
// 如果readonly包了一个ref,这个readonly不是reactive
console.log(isReactive(user2));
console.log(isReactive(user));
console.log(isReactive(foo2));
console.log(isReactive(foo));
console.log(isReactive(info));
console.log(isReactive(xx));
</script>
<style lang="less" scoped>
</style>
isProxy
作用:判断一个变量是不是 readonly 或 reactive的。
// Demo.vue
<template>
<h1>isProxy的使用</h1>
<h1 v-text="user2.a"></h1>
<button @click="add">+1</button>
</template>
<script setup>
import { reactive, readonly, ref, isReactive,isProxy } from "vue";
const uu = { a: 1, b: 2 };
const user = reactive(uu);
const info = readonly({ c: 3, d: 4 }); // 是Readonly
const user2 = readonly(user); // 是Readonly
const foo = ref(100); // foo是一个ref,不是reactive
const foo2 = readonly(foo); // 是Readonly
const xx = 100;
const add = () => {
user2.a++;
};
// isProxy判断一个变量是否是readonly或reactive
console.log(isProxy(uu));
console.log(isProxy(user));
console.log(isProxy(info));
console.log(isProxy(user2));
console.log(isProxy(foo));
console.log(isProxy(foo2));
console.log(isProxy(xx));
</script>
<style lang="less" scoped>
</style>
toRaw
作用:得到返回 reactive变量或 readonly变量的"原始对象"。
语法:const raw = toRaw(reactive变量或readonly变量)
说明:reactive(obj)、readonly(obj) 和 obj 之间是一种代理关系,并且它们之间是一种浅拷贝的关系。obj 变化,会导致reactive(obj) 同步变化,反之一样。
// Demo.vue
<template>
<h1>toRaw的使用</h1>
</template>
<script setup>
import { reactive, toRaw, readonly } from "vue";
const uu = { a: 1, b: 2 }; // 原始对象
const user = reactive(uu); // 把uu变成了reactive
const info = readonly(user); // 把reactive变成readonly
// toRaw(user) 把一个reactive打回原形 变成原始对象
// 它们之间的转化是浅copy
// toRaw(user) 和 uu 始终是同一个对象
console.log(toRaw(user) === uu); // true
// reactive对象和原始对象比较:false
console.log(user === uu);
console.log("-----------");
// toRaw(info) 把一个readmonly打回原形 还是得原始对象
console.log(toRaw(info) === uu);
</script>
markRaw
作用:把一个普通对象标记成"永久原始",从此将无法再变成proxy了。
语法:const raw = markRaw({a, b})
// Demo.vue
<template>
<h1>markRaw的使用</h1>
<h2>{{ yy2.a }}</h2>
<button @click="add">+1</button>
</template>
<script setup>
import { reactive, markRaw, isReactive } from "vue";
// { a: 1, b: 2 } 就是原始对象
// markRaw({ a: 1, b: 2 }) 把原始对象标记为不可代理
// 不能是raactive或readonly
const user = markRaw({ a: 1, b: 2 });
// yy2不是reactive 因为user被标记为永久原始
const yy2 = reactive(user);
console.log(isReactive(yy2));
// 不是一个reactive,不是响应式的
const add = () => {
yy2.a++;
};
</script>
shallowReactive
作用:定义一个reactive变量,只对它的第一层进行Proxy, 所以只有第一层变化时视图才更新。
语法:const obj = shallowReactive({a:{b:9}})
// Demo.vue
<template>
<h1>shallowReactive的使用</h1>
<h2>{{ aa }}</h2>
<button @click="change">change</button>
</template>
<script setup>
import { shallowReactive } from "vue";
// { a: { b: { c: 1 } } } 利用shallowReactive,只能对第1层代理
// 所谓的第1层就是a这个属性
let aa = shallowReactive({ a: { b: { c: 1 } } });
const change = () => {
// aa = {d:{e:{f:1000}}}
// aa.a = { b: { c: 2 } };
// aa.a.b = {c:110}
// aa.a.b.c = 110
aa.a.b.c++
};
</script>
shallowReadonly
作用:定义一个reactive变量,只有第一层是只读的。
语法:const obj = shallowReadonly({a:{b:9}})
// Demo.vue
<template>
<h1>shallowReadonly的使用</h1>
<h2>{{ mm }}</h2>
<button @click="change">change</button>
</template>
<script setup>
import { shallowReadonly, reactive } from "vue";
// shallowReadonly只有第1层是只读的
// 第一层:a d
// 第二层:b
// 第三层:c
const mm = shallowReadonly(reactive({ a: { b: { c: 1 } }, d: 2 }));
const change = () => {
// mm.d++ // 不能改
mm.a.b.c++; // 可以改
};
</script>
computed
作用:对响应式变量进行缓存计算。
语法:const c = computed(fn / {get, set})
// Demo.vue
<template>
<h1>computed的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<h2>{{ total }}</h2>
<input type="text" v-model.number="total" />
</template>
<script setup>
import { ref, computed } from "vue";
let num = ref(100)
// let total = computed({
// get() {
// return num.value * 100
// },
// set(val) {
// num.value = val / 100
// }
// })
let total = computed(() => {
return 666
})
</script>
watch
作用:用于监听响应式变量的变化,组件初始化时,它不执行。
语法:const stop = watch(x, (new, old)=>{}),调用stop() 可以停止监听。
语法:const stop = watch([x, y], ([newX, newY], [oldX, oldY])=>{}),调用stop()可以停止监听。
watch接收三个参数:
- 参数1:监听的数据源,可以是一个ref获取是一个函数
- 参数2:回调函数(val, oldVal)=> {}
- 参数3:额外的配置 是一个是对象时进行深度监听,添加 { deep:true, immediate: true}
// Demo.vue
<template>
<h1>watch的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<h2>{{ total }}</h2>
<input type="text" v-model.number="total" />
<button @click="stopAll">停止一切监听</button>
</template>
<script setup>
import { ref, computed, watch } from "vue";
let num = ref(100)
const total = computed({
get() {
return num.value * 100;
},
set(val) {
num.value = val / 100;
},
});
// ------------------------------------
let stop1 = watch(num, (newNum, oldNum) => {
console.log("num变化了:", newNum, oldNum);
})
// ------------------------------------
let stop2 = watch([num, total], ([newNum, newTotal], [oldNum, oldTotal]) => {
console.log("num变化了:", newNum, oldNum);
console.log("total变化了:", newTotal, oldTotal);
})
// ------------------------------------
let stopAll = () => {
stop1();
stop2();
}
</script>
watchEffect
作用:相当于是 react中的 useEffect(),用于执行各种副作用。
语法:
- const stop = watchEffect(fn),默认其 flush:‘pre’,前置执行的副作用。
- watchPostEffect,等价于 watchEffect(fn, {flush:‘post’}),后置执行的副作用。
- watchSyncEffect,等价于 watchEffect(fn, {flush:‘sync’}),同步执行的副作用。
- 特点:watchEffect 会自动收集其内部响应式依赖,当响应式依赖发变化时,这个watchEffect将再次执行,直到你手动 stop() 掉它。
// Demo.vue
<template>
<h1>watchEffect的使用</h1>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
<hr />
<h1 v-text="foo"></h1>
<button @click="foo--">自减</button>
<hr />
<h1 v-text="bar.a"></h1>
<button @click="bar.a++">改变</button>
</template>
<script setup>
import { ref, computed, watchEffect, watchPostEffect, reactive } from "vue";
let num = ref(1)
let foo = ref(200)
let bar = reactive({ a: 1, b: 2 })
// watchEffect 处理各种副作用 类似于生命周期函数
// 下面的代码,一开始只会执行一次,后面不会执行
// watchEffect(() => {
// console.log("执行副作用1");
// })
// 当num数据发生了变化,需要重新执行,你需要在下面的代码使用num
// watchEffect依赖了num数据,num数据变化,就会重新执行
// watchEffect(() => {
// console.log("执行副作用1",num.value);
// })
// 只能foo数据变化,才会重新执行
// watchEffect(() => {
// console.log("执行副作用1", foo.value);
// })
// watchEffect(() => {
// console.log("执行副作用1", foo.value,num.value);
// })
// --------------------------------------------
// watchEffect(() => { // pre先执行
// console.log("执行副作用1", foo.value, num.value);
// }, { flush: "pre" })
// watchEffect(() => { // post表示后执行
// console.log("执行副作用2", foo.value, num.value);
// }, { flush: "post" })
// --------------------------------------------
// // 这样写等价与 { flush: "pre" })
// watchEffect(() => { // pre先执行
// console.log("执行副作用1", foo.value, num.value);
// })
// // 这样写等价与 { flush: "post" })
// watchPostEffect(() => { // post表示后执行
// console.log("执行副作用2", foo.value, num.value);
// })
// --------------------------------------------
// 1 3 2
// watchEffect(() => { // pre先执行
// console.log("执行副作用1", foo.value, num.value);
// }, { flush: "pre" })
// watchEffect(() => { // post表示后执行
// console.log("执行副作用2", foo.value, num.value);
// }, { flush: "post" })
// // sync 同步执行,轮到它的时候就执行
// watchEffect(() => { // post表示后执行
// console.log("执行副作用3", foo.value, num.value);
// }, { flush: "sync" });
// --------------------------------------------
// 3 1 2
// sync 同步执行,轮到它的时候就执行
// 等价于:watchSyncEffect
watchEffect(() => { // post表示后执行
console.log("执行副作用3", foo.value, num.value);
}, { flush: "sync" });
watchEffect(() => { // pre先执行
console.log("执行副作用1", foo.value, num.value);
}, { flush: "pre" })
watchEffect(() => { // post表示后执行
console.log("执行副作用2", foo.value, num.value);
}, { flush: "post" })
</script>
生命周期钩子
- 选项式的 beforeCreate、created,被setup替代了。setup表示组件被创建之前、props被解析之后执行,它是组合式 API 的入口。
- 选项式的 beforeDestroy、destroyed 被更名为 beforeUnmount、unmounted。
- 新增了两个选项式的生命周期 renderTracked、renderTriggered,它们只在开发环境有用,常用于调试。
- 在使用 setup组合时,不建议使用选项式的生命周期,建议使用 on* 系列 hooks生命周期。
// Demo.vue
<template>
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
</template>
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onRenderTracked,
onRenderTriggered,
onActivated,
onDeactivated,
onErrorCaptured,
} from "vue";
console.log("---setup");
const num = ref(100);
// 挂载阶段
onBeforeMount(() => console.log("---开始挂载"));
onRenderTracked(() => console.log("---跟踪"));
onMounted(() => console.log("---挂载完成"));
// 更新阶段
onRenderTriggered(() => console.log("---触发"));
onBeforeUpdate(() => console.log("---开始更新"));
onUpdated(() => console.log("---更新完成"));
// 销毁阶段
onBeforeUnmount(() => console.log("---开始销毁"));
onUnmounted(() => console.log("---销毁完成"));
// 与动态组件有关
onActivated(() => console.log("---激活"));
onDeactivated(() => console.log("---休眠"));
// 异常捕获
onErrorCaptured(() => console.log("---错误捕获"));
</script>
provide / inject
作用:在组件树中自上而下地传递数据.
语法:provide(‘key’, value)
语法:const value = inject(‘key’, ‘默认值’)
// App.vue
<template><Demo /></template>
<script setup>
import Demo from "./components/Demo.vue";
import { ref, provide } from "vue";
const msg = ref("Hello World");
// 向组件树中注入数据
provide("msg", msg);
</script>
// Demo.vue
<template>
<h1>provide / inject的使用</h1>
<h1 v-text="msg"></h1>
</template>
<script setup>
import { inject } from "vue";
// 消费组件树中的数据,第二参数为默认值
// inject表示注入,消费者
// "Hello Vue" 是默认值
const msg = inject("msg", "Hello Vue");
</script>
getCurrentInstance
作用:用于访问内部组件实例。请不要把它当作在组合式 API 中获取 this 的替代方案来使用。
语法:const app = getCurrentInstance()
场景:常用于访问 app.config.globalProperties 上的全局数据。
// main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
let app = createApp(App)
// Vue2.x
// Vue.prototype.$http = () => { }
// Vue3.x
app.config.globalProperties.$http = () => { }
app.mount('#app')
// Demo.vue
<template>
<h1>getCurrentInstance的使用</h1>
</template>
<script setup>
import { getCurrentInstance } from "vue";
const app = getCurrentInstance();
// 全局数据,是不具备响应式的。
const global = app.appContext.config.globalProperties;
console.log("app", app);
console.log("全局数据", global);
</script>
组合编程最佳范式
最佳实践:
- 只使用 setup 及组合API,不要再使用vue选项了。
- 有必要封装 hooks时,建议把功能封装成hooks,以便于代码的可维护性。
- 能用 vite就尽量使用vite,能用ts 就尽量使用ts。
总结常用API如下:
- ref
- reactive
- watch
- computed
- watchEffect
- toRefs
- onMounted
- onBeforeUnmount
- onActivated
- onDeactivated
- 其它的组合API都是面试题
- provide / inject 用setup语法糖组合时是有响应式的,如果用选项式写法是没有响应式的(需要使用computed包裹一层)
todomvc
参考:https://www.cnblogs.com/OwenLin/p/14062857.html
三、Vue3组件通信
props
props主要用于父组件向子组件通信。在父组件中通过用 :msg=“msg” 绑定需要传给子组件的属性值,然后再在子组件中用 props 接收该属性值。
父组件App.vue:
<template><Demo :msg1="msg1" :msg2="msg2" /></template>
<script setup>
import Demo from "./components/Demo.vue";
import { ref, reactive } from "vue";
//基础类型传值
const msg1 = ref("码路");
// 复杂类型(数组或对象)传值
const msg2 = reactive(["漫漫", "前端"]);
</script>
子组件Demo.vue:
<template>
<h1>父组件传给子组件的值</h1>
<h2 v-text="msg1"></h2>
<h2 v-text="msg2"></h2>
</template>
<script setup>
import { defineProps } from "vue";
// 这里不需要在从vue中引入defineProps,直接用
defineProps({
// 第一种写法
msg1: String,
// 第二种写法
msg2: {
type: String,
default: "",
},
});
</script>
$emit
- $emit 也就是通过自定义事件传值,主要用于子组件向父组件通信。
- 在子组件的点击事件中,通过触发父组件中的自定义事件,把想传给父组件的信息以参数的形式带过去,父组件便可以拿到子组件传过来的参数值。
父组件App.vue:
<template><Demo @myClick="onMyClick" /></template>
<script setup>
import Demo from "./components/Demo.vue";
// 父组件接受到子组件传过来的值
const onMyClick = (msg) => {
console.log(msg);
};
</script>
子组件Demo.vue:
<template>
<h1>$emit</h1>
<button @click="emit('myClick', '1000万')">按钮</button>
</template>
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits("myClick");
</script>
<!-- 或如下: -->
<template>
<h1>$emit</h1>
<button @click="handleClick">按钮</button>
</template>
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits("myClick");
const handleClick = () => {
emit("myClick", "1000万");
};
</script>
ref
有时候想访问refs绑定的组件的属性或者方法,我们会使用refs绑定的组件的属性或者方法,我们会使用refs绑定的组件的属性或者方法,我们会使用refs。但是Vue3不同于Vue2,在 Vue3的setup中我们是无法访问到this的,所以我们需要借助一个方法,那就是getCurrentInstance,该方法返回了当前的实例对象。
父组件App.vue:
<template>
<Demo ref="childRef" />
<button @click="show">show child message</button>
</template>
<script setup>
import Demo from "./components/Demo.vue";
import { ref } from "vue";
const childRef = ref(null);
let show = () => {
let child = childRef.value;
console.log(child.msg);
child.alertMessage();
};
</script>
子组件Demo.vue:
<template>
<h1>Ref</h1>
</template>
<script setup>
import { ref, defineExpose } from "vue";
let msg = ref("666").value;
let alertMessage = () => {
alert(msg);
};
defineExpose({
msg,
alertMessage,
});
</script>
注意: 通过组合语法糖的写法,其组件是默认关闭的,也就是说如果是通过$refs
或者$parents
来访问子组件中定义的值是拿不到的,必须通过defineExpose
向外暴露你想获取的值才行
provide/inject
provide与inject 主要为父组件向子组件或多级嵌套的子组件通信。
- provide:在父组件中可以通过 provide 提供需要向后代组件传送的信息。
- inject:从父组件到该组件无论嵌套多少层都可以直接用 inject 拿到父组件传送的信息。
父组件App.vue:
<template>
<Demo ref="childRef" />
</template>
<script setup>
import Demo from "./components/Demo.vue";
import { provide } from "vue";
// 父组件通过 provide 提供给子组件的信息
provide("car", "奔驰");
</script>
子组件Demo.vue:
<template>
<h1>provide / inject</h1>
</template>
<script setup>
import { inject } from "vue";
// 后代组件通过 inject 可以直接拿到父组件提供的信息
const car = inject("car");
console.log(car);
</script>
eventBus
Vue 3 中移除了 eventBus,但可以借助第三方工具来完成。Vue 官方推荐使用 mitt 或 tiny-emitter。
vuex/pinia
更加推荐使用pinia状态管理
v-model
四、Hooks封装
为什么要封装Hooks
- 在Vue2中,在同一个.vue组件中,当 data、methods、computed、watch 的体量较大时,代码将变得臃肿
- 在vue3,使用 Hooks封装来解决
- 所谓 Hooks封装,就是把不同的逻辑关注点抽离出来,以达到业务逻辑的独立性
如何封装Hooks
- 在 setup 组合的开发模式下,把具体某个业务功能所用到的 ref、reactive、watch、computed、watchEffect 等,提取到一个以 use* 开头的自定义函数中去。
- 封装成 use* 开头的Hooks函数,不仅可以享受到封装带来的便利性,还有利于代码逻辑的复用。Hooks函数的另一个特点是,被复用时可以保持作用域的独立性,即,同一个Hooks函数被多次复用,彼此是不干扰的。
在哪些情况下需要封装Hooks
- 两种场景:一种是功能类Hooks,即为了逻辑复用的封装;另一种是业务类Hooks,即为了逻辑解耦的封装。下面我给两组代码,说明这两种使用场景。
实践
- useCounter
// src/hooks/useCounter.ts
import { ref } from "vue"
export default function(){
let counter = ref(0)
let add = ()=>counter.value++
let sub = ()=>counter.value--
return {
counter,
add,
sub
}
}
组件中使用之,如下:
// Demo.vue
<template>
<h1>useCounter</h1>
<h4>{{ counter }}</h4>
<button @click="add">+1</button>
<button @click="sub">-1</button>
</template>
<script setup>
import useCounter from "./hooks/useCounter";
let { counter, add, sub } = useCounter();
</script>
- useTitle
/ src/hooks/useTitle.ts
import {ref,watch} from "vue"
export default function(title="默认的title"){
let titleRef = ref(title)
watch(titleRef,(newValue,oldNew)=>{
document.title = newValue;
},{
immediate:true
})
return titleRef;
}
组件中使用之,如下:
// Demo.vue
<template>
<h1>useTitle</h1>
</template>
<script setup>
import useTitle from "./hooks/useTitle";
let titleRef = useTitle();
setTimeout(() => {
titleRef.value = "hi vue3";
}, 3000);
</script>
- useScrollPosition
// src/hooks/useScrollPosition.ts
import { ref } from "vue"
export default function(){
let scrollX = ref(0)
let scrollY = ref(0)
document.addEventListener("scroll",()=>{
scrollX.value = window.scrollX
scrollY.value = window.scrollY
})
return {
scrollX,
scrollY
};
}
组件中使用之,如下:
// Demo.vue
<template>
<h1>useScrollPosition</h1>
<div class="content"></div>
<div class="scroll">
<div class="scroll-x">scrollX:{{ scrollX }}</div>
<div class="scroll-y">scrollY:{{ scrollY }}</div>
</div>
</template>
<script setup>
import useScrollPosition from "./hooks/useScrollPosition";
let { scrollX, scrollY } = useScrollPosition();
</script>
<style scoped>
.content {
width: 3000px;
height: 5000px;
}
.scroll {
position: fixed;
right: 30px;
bottom: 30px;
}
</style>
- useMousePosition
// src/hooks/useMousePosition.ts
import { ref } from "vue"
export default function(){
let mouseX = ref(0)
let mouseY = ref(0)
window.addEventListener("mousemove",(e)=>{
mouseX.value = e.pageX;
mouseY.value = e.pageY;
})
return {
mouseX,
mouseY,
};
}
组件中使用之,如下:
// Demo.vue
<template>
<h1>useMousePosition</h1>
<div class="mouse">
<div class="mouse-x">mouseX:{{ mouseX }}</div>
<div class="mouse-y">mouseY:{{ mouseY }}</div>
</div>
</template>
<script setup>
import useMousePosition from "./hooks/useMousePosition";
let { mouseX, mouseY } = useMousePosition();
</script>
- useLocalStoage
// src/hooks/useLocalStoage.ts
import { ref, watch } from "vue"
// useLocalStoage("token","68656789fsadfsadf78765fasdfadsf")
// useLocalStoage("token")
export default function(key,value){
let data = ref(value)
if(value){
// 存
window.localStorage.setItem(key,JSON.stringify(value))
}else{
// 取
data.value = JSON.parse(window.localStorage.getItem(key))
}
watch(data,(newValue)=>{
window.localStorage.setItem(key,JSON.stringify(newValue))
})
return data;
}
组件中使用之,如下:
// Demo.vue
<template>
<h1>useMousePosition</h1>
<button @click="changeData">修改数据</button>
</template>
<script setup>
import useLocalStoage from "./hooks/useLocalStoage";
let data = useLocalStoage("token", "123456789");
console.log("data:", data);
let changeData = () => (data.value = "987654321");
</script>
五、Vue3语法变化
1,在Vue2中,v-for 和 ref 同时使用,这会自动收集 $refs
。当存在嵌套的v-for时,这种行为会变得不明确且效率低下。在Vue3中,v-for 和 ref 同时使用,这不再自动收集$refs
。我们可以手动封装收集 ref 对象的方法,将其绑定在 ref 属性上。
// Demo.vue
<template>
<h1>ref</h1>
<!-- ref属性和v-for一起使用,需要手动封装方法来收集这些DOM -->
<div v-for="i in 5" :key="i" v-text="i" :ref="collect"></div>
<!-- ref属性使用单一节点上,需要声明一个ref对象来接收DOM -->
<h1 ref="hello">你好</h1>
</template>
<script setup>
import { onMounted, ref } from "vue";
const hello = ref();
const arr = [];
const collect = (ref) => {
if (ref) arr.push(ref);
};
console.log("arr:", arr);
console.log("hello:", hello);
onMounted(() => {
arr[2].style.color = "red";
hello.value.style.color = "blue";
});
</script>
2,在Vue3中,使用 defineAsyncComponent 可以异步地加载组件。需要注意的是,这种异步组件是不能用在Vue-Router的路由懒加载中。
// Demo.vue
<template>
<h1>Demo组件</h1>
</template>
<script setup>
</script>
// App.vue
<template>
<AsyncChild />
</template>
<script setup>
import { defineAsyncComponent } from "vue";
// 异步加载组件
const AsyncChild = defineAsyncComponent({
loader: () => import("./components/Demo.vue"),
delay: 200,
timeout: 3000,
});
</script>
3,Vue3.0中的 $attrs
,包含了父组件传递过来的所有属性,包括 class 和 style 。在Vue2中,$attrs
是接到不到 class 和 style 的。在 setup 组件中,使用 useAttrs() 访问;在非 setup组件中,使用 this.$attrs /setupCtx.attrs
来访问。
// Demo.vue
<template>
<!-- 注:当父组件有传递class和style时,这里只能使用单一根节点 -->
<div>
<h1>$attrs</h1>
</div>
</template>
<script setup>
import { useAttrs } from "vue";
const attrs = useAttrs(); // 父组件传递过来的自定义属性们
console.log("attrs:", attrs);
</script>
// App.vue
<template>
<Demo a="1" :b="2" :c="[110]" class="box" style="color: red" />
</template>
<script setup>
import Demo from "./components/Demo.vue";
</script>
4,Vue3中,移除了 $children
属性,要想访问子组件只能使用 ref 来实现了。在Vue2中,我们使用 $children
可以方便地访问到子组件,在组件树中“肆意”穿梭。
5,Vue3中,使用 app.directive() 来定义全局指令,并且定义指令时的钩子函数们也发生了若干变化。
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
let app = createApp(App)
// 全局指令
app.directive('upper', {
// v3中新增的
created() { },
// 相当于v2中的 bind()
beforeMount() {
},
// 相当于v2中的 inserted()
mounted(el, binding, vnode, prevVnode) {
console.log("----");
console.log(binding);
console.log(el);
el.innerHTML = binding.value.toUpperCase();
},
// v3中新增的
// beforeUpdate() { },
// 相当于v2中的 update()+componentUpdated()
// updated() { },
// v3中新增的
// beforeUnmount() { },
// 相当于v2中的 unbind()
// unmounted() { }
})
app.mount('#app')
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
let app = createApp(App)
// 全局指令
app.directive('upper', {
// v3中新增的
created() { },
// 相当于v2中的 bind()
beforeMount() {
},
// 相当于v2中的 inserted()
mounted(el, binding, vnode, prevVnode) {
console.log("----");
console.log(binding);
console.log(el);
el.innerHTML = binding.value.toUpperCase();
},
// v3中新增的
// beforeUpdate() { },
// 相当于v2中的 update()+componentUpdated()
// updated() { },
// v3中新增的
// beforeUnmount() { },
// 相当于v2中的 unbind()
// unmounted() { }
})
app.mount('#app')
6,data 选项,只支持工厂函数的写法,不再支持对象的写法了。在Vue2中,创建 new Vue({ data }) 时,是可以写成对象语法的。
// Demo.vue
<script>
import { createApp } from 'vue'
createApp({
data() {
return {
msg: 'Hello World'
}
}
}).mount('#app')
</script>
7,Vue3中新增了 emits 选项。在非script setup写法中,使用 emits选项 接收父组件传递过来的自定义,使用 ctx.emit() 来触发事件。在script setup中,使用 defineEmits 来接收自定义事件,使用 defineProps 来接收自定义事件。
8,Vue3中 移除了 $on / $off / $once 这三个事件 API,只保留了 $emit 。
- 也就是说在vue3中不能使用事件总线
9,Vue3中,移除了全局过滤器(Vue.filter)、移除了局部过滤器 filters选项。取而代之,你可以封装自定义函数或使用 computed 计算属性来处理数据。
- Vue3中没有过滤器了
10,Vue3 现在正式支持了多根节点的组件,也就是片段,类似 React 中的 Fragment。使用片段的好处是,当我们要在 template 中添加多个节点时,没必要在外层套一个 div 了,套一层 div 这会导致多了一层 DOM结构。可见,片段 可以减少没有必要的 DOM 嵌套。
// Demo.vue
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
11,函数式组件的变化:在Vue2中,要使用 functional 选项来支持函数式组件的封装。在Vue3中,函数式组件可以直接用普通函数进行创建。如果你在 vite 环境中安装了 @vitejs/plugin-vue-jsx 插件来支持 JSX语法,那么定义函数式组件就更加方便了。
12,Vue2中的Vue构造函数,在Vue3中已经不能再使用了。所以Vue构造函数上的静态方法、静态属性,比如 Vue.use/Vue.mixin/Vue.prototype 等都不能使用了。在Vue3中新增了一套实例方法来代替,比如 app.use()等。
// Demo.vue
import { createApp } from 'vue'
import router from './router'
import store from './store'
import App from './App.vue'
const app = createApp(App)
// 相当于 v2中的 Vue.prototype
app.config.globalProperties.$http = ''
// 等价于 v2中的 Vue.use
app.use(router) // 注册路由系统
app.use(store) // 注册状态管理
13,在Vue3中,使用 getCurrentInstance 访问内部组件实例,进而可以获取到 app.config 上的全局数据,比如 r o u t e 、 route、 route、router、$store 和自定义数据等。这个 API 只能在 setup 或 生命周期钩子 中调用。
14,我们已经知道,使用 provide 和 inject 这两个组合 API 可以组件树中传递数据。除此之外,我们还可以应用级别的 app.provide() 来注入全局数据。在编写插件时使用 app.provide() 尤其有用,可以替代app.config.globalProperties。
// main.tx
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
let app = createApp(App)
// 注册全局数据
app.provide('$url', 'http://localhost:8888')
app.mount('#app')
// Demo.vue
<template>
<div>
<h1>app.provide()注入数据</h1>
</div>
</template>
<script setup>
import { inject } from "vue";
const $url = inject("$url");
console.log("$url:", $url);
</script>
15,在Vue2中,Vue.nextTick() / this.$nextTick 不能支持 Webpack 的 Tree-Shaking 功能的。在 Vue3 中的 nextTick ,考虑到了对 Tree-Shaking 的支持。
- nextTick
- Vue在观察到数据变化时,并不是直接更新DOM,而是开启一个队列,并且缓存同一轮事件循环中的所有数据改变。在缓冲时会除去重复的操作, 等到下一轮事件循环时,才开始更新。
- $nextTick的作用:就是用来告知DOM什么时候更新完,当DOM更新完毕后,nextTick方法里面的回调就会执行
需求: 有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是 false,直接去获取div内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为 true,div才会被创建,此时再去获取。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
let vm = new Vue({
el: "#app",
data: {
showDiv: false
},
methods: {
// 如果像注释的代码,不加nextTick这个方法,就会报错,因为DOM更新是在下一次事件循环,才更新,所以此时获取不到div元素。
// getText: function () {
// this.showDiv = true;
// var text = document.getElementById('div').innerHTML;
// console.log(text);
// }
getText: function() {
this.showDiv = true;
this.$nextTick(function() {
var text = document.getElementById('div').innerHTML;
console.log(text);
});
}
}
});
</script>
</body>
</html>
应用场景
- mounted 获取ajax数据,后根据页面渲染数据的样式(文字占宽)来修改布局。
- 几乎所有更新数据后操作dom的操作,都需要用到异步更新队列
Vue3.x中使用
// Demo.vue
<template>
<h1>Vue3中nextTick</h1>
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</template>
<script setup>
import { ref, nextTick } from "vue";
let showDiv = ref(false);
let getText = () => {
showDiv.value = true;
nextTick(() => {
console.log(document.getElementById("div").innerHTML);
});
};
</script>
16,Vue3中,对于 v-if/v-else/v-else-if的各分支项,无须再手动绑定 key了, Vue3会自动生成唯一的key。因此,在使用过渡动画transition对多个节点进行显示隐藏时,也无须手动加 key了。
// Demo.vue
<template>
<!-- 穿梭框,把内部元素插入to属性所对应的DOM节点中去 -->
<teleport to="head">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css"
/>
</teleport>
<div>
<transition
enter-active-class="animate__animated animate__bounce"
leave-active-class="animate__animated animate__backOutDown"
mode="out-in"
>
<!-- <transition name="ml" mode="out-in"> -->
<h1 v-if="bol">白天</h1>
<h1 v-else>黑夜</h1>
</transition>
</div>
<button @click="toggle">切换</button>
<teleport to="body">
<h1>你好</h1>
</teleport>
</template>
<script setup>
import { ref } from "vue";
const bol = ref(true);
const toggle = () => {
bol.value = !bol.value;
};
</script>
<style>
.ml-enter-from {
opacity: 0;
color: red;
}
.ml-enter-active {
transition: all ease 2s;
}
.ml-enter-to {
opacity: 1;
color: black;
}
.ml-leave-from {
opacity: 1;
color: black;
}
.ml-leave-active {
transition: all ease 2s;
}
.ml-leave-to {
opacity: 0;
color: blue;
}
</style>
17,在Vue2中,使用 Vue.config.keyCodes 可以修改键盘码,这在Vue3 中已经淘汰了。
18,Vue3中,$listeners
被移除了。因此我们无法再使用$listeners
来访问、调用父组件给的自定义事件了。
Vue2中的$listeners地址:https://www.jb51.net/article/240305.htm
19,在Vue2中,根组件挂载 DOM时,可以使用 el 选项、也可以使用 $mount()
。但,在 Vue3中只能使用 $mount()
来挂载了。并且,在 Vue 3中,被渲染的应用会作为子元素插入到 div id=‘app’ 中,进而替换掉它的innerHTML。
20,在Vue2中,使用 propsData 选项,可以实现在 new Vue() 时向根组件传递 props 数据。在Vue3中,propsData 选项 被淘汰了。替代方案是:使用createApp的第二个参数,在 app实例创建时向根组件传入 props数据。
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 使用第二参数,向App传递自定义属性
const app = createApp(App, { name:'vue3' })
app.mount('#app') // 挂载
// App.vue
<script setup>
import { defineProps } from 'vue'
// 接收 createApp() 传递过来的自定义属性
const props = defineProps({
name: { type: String, default: '' }
})
console.log('app props', props);
</script>
21,在Vue2中,组件有一个 render 选项(它本质上是一个渲染函数,这个渲染函数的形参是 h 函数),h 函数相当于 React 中的 createElement()。在Vue3中,render 函数选项发生了变化:它的形参不再是 h 函数了。h 函数变成了一个全局 API,须导入后才能使用。
虚拟DOM的渲染过程
认识h函数 Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
- Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM)
- 事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode
- 那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode
- h() 函数是一个用于创建 vnode 的一个函数
- 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数
代码如下:
// Demo.vue
<template>
<renderButton></renderButton>
</template>
<script setup>
// h是用来创建虚拟节点
import {
h
} from "vue";
let renderButton = {
// render叫渲染函数
render() {
return h("h2", {
class: "title"
}, "Hello Vue");
},
};
</script>
// Demo.vue
<template>
<renderButton></renderButton>
</template>
<script setup>
// h是用来创建虚拟节点
import { h, ref } from "vue";
let counter = ref(0);
let renderButton = {
// render叫渲染函数
render() {
return h("div", { class: "app" }, [
h("h2", null, `计数器:${counter.value}`),
h(
"button",
{
onClick: () => counter.value++,
},
"加1"
),
h(
"button",
{
onClick: () => counter.value--,
},
"减1"
),
]);
},
};
</script>
22,Vue3中新增了实验性的内置组件suspense,它类似 React. Suspense 一样,用于给异步组件加载时,指定 Loading指示器。需要注意的是,这个新特征尚未正式发布,其 API 可能随时会发生变动。
// Demo.vue
<template>
<suspense>
<!-- 用name='default'默认插槽加载异步组件 -->
<AsyncChild />
<!-- 异步加载成功前的loading 交互效果 -->
<template #fallback>
<div> Loading... </div>
</template>
</suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncChild = defineAsyncComponent({
loader: ()=>import('./components/Child.vue'),
delay: 200,
timeout: 3000
});
</script>
23,Vue3中,过渡动画transition发生了一系列变化。之前的 v-enter 变成了现在的 v-enter-from , 之前的 v-leave 变成了现在的 v-leave-from 。
// Demo.vue
<template>
<transition name="fade">
<h1 v-if="bol">但使龙城飞将在,不教胡马度阴山!</h1>
</transition>
<button @click="bol = !bol">切换</button>
</template>
<script setup>
import { ref } from "vue";
const bol = ref(true);
</script>
<style scoped>
.fade-enter-from {
opacity: 0;
color: red;
}
.fade-enter-active {
transition: all 1s ease;
}
.fade-enter-to {
opacity: 1;
color: black;
}
.fade-leave-from {
opacity: 1;
color: black;
}
.fade-leave-active {
transition: all 1.5s ease;
}
.fade-leave-to {
opacity: 0;
color: blue;
}
</style>
24,在Vue3中,v-on的.native修饰符已被移除。
25,同一节点上使用 v-for 和 v-if ,在Vue2中不推荐这么用,且v-for优先级更高。在Vue3中,这种写法是允许的,但 v-if 的优秀级更高。
26,在Vue2中,静态属性和动态属性同时使用时,不确定最终哪个起作用。在Vue3中,这是可以确定的,当动态属性使用 :title 方式绑定时,谁在前面谁起作用;当动态属性使用 v-bind='object’方式绑定时,谁在后面谁起作用。
// Demo.vue
<template>
<!-- 这种写法,同时绑定静态和动态属性时,谁在前面谁生效! -->
<div id='red' :id='("blue")'>不负当下</div>
<div :title='("hello")' title='world'>不畏未来</div>
<hr>
<!-- 这种写法,同时绑定静态和动态属性时,谁在后面谁生效! -->
<div id='red' v-bind='{id:"blue"}'>不负当下</div>
<div v-bind='{title:"hello"}' title='world'>不畏未来</div>
</template>
27,当使用watch选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定deep选项。
// Demo.vue
<template>
<div v-for="t in list" v-text="t.task"></div>
<button @click.once="addTask">添加任务</button>
</template>
<script setup>
import { reactive, watch } from "vue";
const list = reactive([
{ id: 1, task: "读书", value: "book" },
{ id: 2, task: "跑步", value: "running" },
]);
const addTask = () => {
list.push({ id: 3, task: "学习", value: "study" });
};
// 当无法监听一个引用类型的变量时
// 添加第三个选项参数 { deep:true }
watch(
list,
() => {
console.log("list changed", list);
},
{ deep: true }
);
setTimeout(() => {
list[0].task += "666";
}, 2000);
</script>
28,在Vue2中接收 props时,如果 prop的默认值是工厂函数,那么在这个工厂函数里是有 this的。在Vue3中,生成 prop 默认值的工厂函数不再能访问this了。
// Demo01.vue
<template>
<h1>Demo01组件</h1>
</template>
<script setup>
import { defineProps, inject } from "vue";
const props = defineProps({
a: { type: Number, default: 0 },
b: { type: Boolean, default: false },
// 如果自定义属性是引用数据类型,default建议使用如下写法(工厂函数)
c: {
type: Array,
default() {
return inject("list", []);
},
},
d: {
type: Object,
default() {
return inject("info", { name: "码路", age: 10 });
},
},
});
console.log("props:", props);
</script>
// Demo.vue
<template>
<h1>Demo组件</h1>
<Demo01 :a="1" b :c="list" />
</template>
<script setup>
import Demo01 from "./Demo01.vue";
import { reactive, provide } from "vue";
const list = reactive([1, 2, 3]);
const info = reactive({ a: 1, b: 2 });
provide("list", [4, 5, 6]);
provide("info", { c: 3, d: 4 });
</script>
29,Vue3中,新增了teleport组件,这相当于 ReactDOM.createPortal(),它的作用是把指定的元素或组件渲染到任意父级作用域的其它DOM节点上。上面第 16个知识点中,用到了teleport加载 animate.css 样式表,这算是一种应用场景。除此之外,teleport还常用于封装 Modal 弹框组件。
30,在Vue3中,移除了 model 选项,移除了 v-bind 指令的 .sync 修饰符。在Vue2中,v-model 等价于 :value + @input ;在Vue3中,v-model 等价于 :modelValue + @update:modelValue 。在Vue3中,同一个组件上可以同时使用多个 v-model。在Vue3中,还可以自定义 v-model 的修饰符。
v-model在组件上的简单使用:
// Demo.vue
<template>
<h1>页面</h1>
<hr />
<!-- v-model = :modelValue + @update:modelValue -->
<!-- v-model:gender = :gender + @update:gender -->
<!-- v-model:xxx = :xxx + @update:xxx -->
<!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
<Demo01 v-model="lang" />
</template>
<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
</script>
// Demo01.vue
<template>
<div>
<div class="lang">
<span
v-for="item in langs"
:key="item.id"
v-text="item.label"
:class="{ on: modelValue === item.value }"
@click="langChange(item)"
>
</span>
</div>
</div>
</template>
<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
{ id: 1, value: "zh", label: "中文" },
{ id: 2, value: "en", label: "英文" },
{ id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
modelValue: { type: String, default: "zh" },
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);
const langChange = (item) => {
// 如果sort为真,先处理排序,再回传给父组件
// 如果trim为真,把字符串首尾空字符删除,再回传父组件
emit("update:modelValue", item.value);
};
</script>
<style>
.lang span {
cursor: pointer;
padding: 20px;
}
.lang span.on {
color: red;
}
</style>
多个v-model的使用
// Demo.vue
<template>
<h1>v-model在组件上的使用</h1>
<hr />
<!-- v-model = :modelValue + @update:modelValue -->
<!-- v-model:gender = :gender + @update:gender -->
<!-- v-model:xxx = :xxx + @update:xxx -->
<!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
<Demo01 v-model="lang" v-model:gender="gender" />
</template>
<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
const gender = ref("unknow");
</script>
// Demo01.vue
<template>
<div>
<div class="lang">
<span
v-for="item in langs"
:key="item.id"
v-text="item.label"
:class="{ on: modelValue === item.value }"
@click="langChange(item)"
>
</span>
</div>
<div>
<!-- 对H5的单选按钮组来讲, v-model = :checked + @change -->
<!-- 对H5的文本表单来讲,v-model = :value + @input -->
<!-- 对H5的下拉框来讲,v-model = :value + @change -->
<input
type="radio"
value="man"
:checked="gender === 'man'"
@change="emit('update:gender', $event.target.value)"
/>男 <input type="radio" value="woman" v-model="g" />女
<input type="radio" value="unknow" v-model="g" />保密
</div>
</div>
</template>
<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
{ id: 1, value: "zh", label: "中文" },
{ id: 2, value: "en", label: "英文" },
{ id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
modelValue: { type: String, default: "zh" },
modelModifiers: { default: () => ({}) },
gender: { type: String, default: "man" },
genderModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue", "update:gender"]);
const g = computed({
get() {
return props.gender;
},
set(val) {
emit("update:gender", val);
},
});
const langChange = (item) => {
emit("update:modelValue", item.value);
};
</script>
<style>
.lang span {
cursor: pointer;
padding: 20px;
}
.lang span.on {
color: red;
}
</style>
自定义修饰符,如下:
// Demo.vue
<template>
<h1>页面</h1>
<hr />
<!-- v-model = :modelValue + @update:modelValue -->
<!-- v-model:gender = :gender + @update:gender -->
<!-- v-model:xxx = :xxx + @update:xxx -->
<!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
<Demo01 v-model.trim.sort="lang" />
</template>
<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
</script>
// Demo01.vue
<template>
<div>
<div class="lang">
<span
v-for="item in langs"
:key="item.id"
v-text="item.label"
:class="{ on: modelValue === item.value }"
@click="langChange(item)"
>
</span>
</div>
</div>
</template>
<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
{ id: 1, value: "zh", label: "中文" },
{ id: 2, value: "en", label: "英文" },
{ id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
modelValue: { type: String, default: "zh" },
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);
const langChange = (item) => {
const { sort, trim } = props.modelModifiers;
console.log(sort, trim);
// 如果sort为真,先处理排序,再回传给父组件
// 如果trim为真,把字符串首尾空字符删除,再回传父组件
emit("update:modelValue", item.value);
};
</script>
<style>
.lang span {
cursor: pointer;
padding: 20px;
}
.lang span.on {
color: red;
}
</style>
案例
// GoodFilter.vue
<template>
<span>请选择商家(多选):</span>
<span v-for="s in shopArr">
<input
type="checkbox"
:value="s.value"
:checked="shop.includes(s.value)"
@change="shopChange"
/>
<span v-text="s.label"></span> </span
><br />
<span>请选择价格(单选):</span>
<span v-for="p in priceArr">
<input
type="radio"
:value="p.value"
:checked="p.value === price"
@change="priceChange"
/>
<span v-text="p.label"></span>
</span>
</template>
<script setup>
import { reactive, defineProps, defineEmits, toRefs } from "vue";
const props = defineProps({
shop: { type: Array, default: [] },
// 接收v-model:shop的自定义修饰符
shopModifiers: { default: () => ({}) },
price: { type: Number, default: 500 },
// 接收v-model:price的自定义修饰符
priceModifiers: { default: () => ({}) },
});
const { shop, price } = toRefs(props);
// 接收v-model的自定义事件
const emit = defineEmits(["update:shop", "update:price"]);
const shopArr = reactive([
{ id: 1, label: "华为", value: "huawei" },
{ id: 2, label: "小米", value: "mi" },
{ id: 3, label: "魅族", value: "meizu" },
{ id: 4, label: "三星", value: "samsung" },
]);
const priceArr = reactive([
{ id: 1, label: "1000以下", value: 500 },
{ id: 2, label: "1000~2000", value: 1500 },
{ id: 3, label: "2000~3000", value: 2500 },
{ id: 4, label: "3000以上", value: 3500 },
]);
// 多选框
const shopChange = (ev) => {
const { checked, value } = ev.target;
// 使用v-model:shop的自定义修饰符
const { sort } = props.shopModifiers;
let newShop = checked
? [...shop.value, value]
: shop.value.filter((e) => e !== value);
if (sort) newShop = newShop.sort();
emit("update:shop", newShop);
};
// 单选框
const priceChange = (ev) => {
emit("update:price", Number(ev.target.value));
};
</script>
<style scoped>
</style>
// App.vue
<template>
<GoodFilter
v-model:shop.sort='shop'
v-model:price='price'
/>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import GoodFilter from './components/GoodFilter.vue'
const shop = ref([])
const price = ref(500)
watch([shop, price], ()=>{
console.log('changed', shop.value, price.value)
});
</script>
31,新增了一个指令v-memo
// Demo.vue
<template>
<h1>v-memo指令</h1>
<!-- 只有当foo这个变量变化时,v-memo所包裹的视图结构才更新 -->
<div v-memo="[foo]">
<h1 v-text="num"></h1>
<button @click="num++">自增</button>
</div>
<hr />
<h1 v-text="foo"></h1>
<button @click="foo--">自减</button>
</template>
<script setup>
import { ref } from "vue";
const num = ref(1);
const foo = ref(1000);
</script>
32, 样式书写的必变
// Demo.vue
<template >
<h1 class="box">样式的变化</h1>
<button @click="change">改变</button>
<hr />
<h1 :class="zz">测试</h1>
<button @click="changeZZ">改变</button>
</template>
<script setup>
import { ref, useCssModule } from "vue";
const style = useCssModule();
const mlstyle = useCssModule("ml");
const cc = ref("red");
const ff = ref(20);
const zz = ref(mlstyle.r1);
const change = () => {
cc.value = "blue";
};
const changeZZ = () => {
zz.value = mlstyle.r2;
};
</script>
<style>
/* 在style标签中,只能使用v-bind */
.box {
color: v-bind(cc);
}
.r1 {
color: orange;
}
</style>
<style scoped module='ml'>
.r1 {
color: red;
}
.r2 {
color: blue;
}
.r3 {
color: green;
}
</style>
总结Vue3.x变化
移除了
- c h i l d r e n 移 除 了 ( 注 意 children移除了(注意 children移除了(注意parent还在)
- new Vue({ data:{} }) 这个写法淘汰了,如果用到data一定要使用工厂函数。
- 移除了 $on / $off / o n c e ( 注 意 once (注意 once(注意emit还在)
- 在V3中,事件总线这种通信这种基于订阅发布模式的通信方案也不能用了。
- 移除了全局过滤器(Vue.filter)、移除了局部过滤器 filters选项。
- 在V3中,Vue这个构造函数不能用了,所以像V2中的那个全局API都不能了,所以Vue的原型链也不能用了。
- 在V3中,不再支持修改键盘码了。
- 移除了 l i s t e n e r s ( 注 意 listeners (注意 listeners(注意attrs还在)
- 移除了 el选项、model选项、propsData选项
- v-on的.native修饰符已被移除。
新增了
- 在V3中,增加了 emits选项、defineEmits,用于在子组件中接收自定义事件。
- 在组件中,使用 getCurrentInstance 可以访问 app根实例。
- 新增了 suspense,用于给异步组件添加交互效果。
- 新增了 teleport的to属性,用于把包裹的视图结构“穿梭”到父级的DOM节点中去。
- 新增了样式的玩法,第一个玩法是在style中可以使用v-bind来做动态样式;在style标签上module='default’实现样式模块化,在组合中使用useCssModule访问样式模块。
- 新增了一个指令 v-memo,用法:在div上v-memo=‘[a, b]’,有且仅有当a或b变化时,其包裹的视图结构才会更新。
- 在V3中,新增了 useSlots,用于在子组件中访问插槽实例。相当于V2中的this.$slots。
变化了
- 在V2中,v-for和ref一起作用,可以自动收集refs对象,在
this.$refs
上访问。在V3中,不能自动收集了,需要自己封装收集方法 - 在V2中,ref属性作用在HTML标签或组件上,可以在
this.$refs
上访问DOM或组件实例;在V3中,ref属性配合ref这个组合API为完成对DOM或组件实例的方法,div ref=‘box’,这个box是一个ref对象,使用box.value访问对应的DOM。 - 在V3中,使用 defineAsyncComponent 定义更加友好的异步组件。
- 在V2中,使用
this.$attrs
访问父组件传递过来的属性们(不包括class和style);在V3中,使用useAttrs/context.attrs/$attrs
访问父组件传递过来的自定义属性们(包含class和style)。 - 在V3中,使用 app.directive({}/fn) 自定义指令,注意在指令的选项钩了发生了若干变化。
- 在V3中,template视图模块支持多节点。(当在自定义组件上使用class/style时、在自定义组件上使用多个v-model时,都报了多节点的警告,这时建议将其改成单一根节点)
- 在V2中,provide/inject是没有响应式的。在V3中,provide用选项写法时,必须配合computed才能实现响应式;provide用组合写法时,默认就有响应式。
- 在V2中,Vue.nextTick() / this.$nextTick 不能支持 Webpack 的 Tree-Shaking 功能的。在 V3 中的 nextTick ,考虑到了对 Tree-Shaking 的支持。
V3中,对于 v-if/v-else/v-else-if的各分支项,无须再手动绑定 key了, V3会自动生成唯一的key。比如transition对多节点执行动画时,无须再加key了。 - 在V3中,render选项的函数参数不再是h函数了,如果要使用h函数,得从vue导入。
- transition过渡动画的变化:v-enter-from/v-enter-active/v-enter-to和V2不同了;对多节点执行显示与隐藏时,不需要再加key了。当我们封装自定义组件时,如果transition是这个组件的根节点,要使用props传值来触发动画。
- 同一节点上使用 v-for 和 v-if ,在V2中不推荐这么用,且v-for优先级更高。在V3中,这种写法是允许的,但 v-if 的优秀级更高。
- watch这个组合API:允许同时监听多个变量的变化,允许停止监听。const stop = watch([a, b], callback)
- 在V3中,使用defineProps接收自定义属性时,如果这个属性是对象类型,其default: () => { return },并且在这个default工厂函数中可以使用inject。
- v-model有三个变化,在自定义组件上v-model = :modelValue + @update:modelValue, v-model:name = :name + @update:name;在自定义组件上,可以同时使用多个v-model,在People 组件上使用v-model=‘age’ v-model:name=‘name’;还支持自定义修饰符,注意在子组件中必须使用defineProps来分别接收自定义修饰符。
六、VueRouter(V4)和ElementPlus
路由的使用
第一步:安装与配置
yarn add vue-router@4
yarn add element-plus
yarn add @element-plus/icons-vue
yarn add sass
在vite.config.ts中配置别名与代理,如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// Vite官方配置
export default defineConfig({
plugins: [vue()],
server: {
port: 8888,
proxy: {
'/api': { // 讲状态管理的时候会用到
target: 'https://cnodejs.org/',
changeOrigin: true
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
第二步:创建路由并配置规则
还没有创建Layout组件
// src/router/index.js
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import Layout from '@/layout/index.vue'
export const asyncRoutes = [
{
id: 1,
text: '学生管理',
icon: 'Watch',
path: '/student',
alias: '/', // 别名
component: Layout,
redirect: '/student/a',
children: [
{ id: 11, text: '学生列表', path: '/student/a', component: () => import('@/pages/student/PageA.vue') },
{ id: 12, text: '添加学生', path: '/student/b', component: () => import('@/pages/student/PageB.vue') }
]
},
{
id: 2,
text: '老师管理',
icon: 'Mouse',
path: '/teacher',
component: Layout,
redirect: '/teacher/a',
children: [
{ id: 21, text: '老师列表', path: '/teacher/a', component: () => import('@/pages/teacher/PageA.vue') },
{ id: 22, text: '添加老师', path: '/teacher/b', component: () => import('@/pages/teacher/PageB.vue') },
]
},
]
const router = createRouter({
history: createWebHashHistory(), // 带#的hash路由模式
// history: createWebHistory(), // 不带#的history路由模式
routes: [
...asyncRoutes,
{ path: '/*', redirect: '/' } // 重定向
]
})
router.beforeEach((to, from, next) => {
next()
})
export default router
第三步:在main.js中注册路由和Elementui plus以及图标
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router/index.js"
// 使用element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 使用字体图标 每个图标,都是一个组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
let app = createApp(App)
// 遍历所有的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
// 全局注册组件
app.component(key, component)
}
// 注册路由
app.use(router)
// 使用elementplus
app.use(ElementPlus)
app.mount('#app')
第四步:创建对应组件
// src/layout.vue
<template>
<el-container>
<el-aside width="200px">
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="2"
text-color="#fff"
>
<el-sub-menu :index="sub.id+''" v-for='sub in asyncRoutes' :key='sub.id'>
<template #title>
<el-icon>
<component :is='sub.icon' />
</el-icon>
<span>{{sub.text}}</span>
</template>
<el-menu-item v-for='item in sub.children' :key='item.id' :index="item.id+''">
<!-- 在路由v4中,router-link已经没有tag属性了,建议使用插槽来改变渲染节点 -->
<!-- 为了实现跳转,下面这个写法是固定的,少一个属性都不行 -->
<router-link :to='item.path' custom v-slot='{navigate}'>
<div @click='navigate'>{{item.text}}</div>
</router-link>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>
<!-- 这里是给所谓的业务页面来显示 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { asyncRoutes } from '@/router'
</script>
<style lang="scss" scoped>
</style>
// src/student/PageA.vue
<template>
<h1>student Page A</h1>
</template>
<script setup>
import { useRoute, useRouter, useLink } from "vue-router";
const route = useRoute(); // 相当于选项式组件中的this.$route.query/params/fullpath...
const router = useRouter(); // 相当于选项式组件中的this.$router.push/replace/back()
console.log("route:", route);
console.log("router:", router);
</script>
// src/student/PageB.vue
<template>
<h1>student Page B</h1>
</template>
// src/teacher/PageA.vue
<template>
<h1>teacher Page A</h1>
</template>
// src/teacher/PageB.vue
<template>
<h1>teacher Page B</h1>
</template>
// App.vue
<template>
<!-- 这个地方是给Layout或登录页用的 -->
<router-view></router-view>
</template>
<script setup>
</script>
<style lang='scss'>
html,
body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#app {
height: 100%;
& > .el-container {
height: 100%;
}
.el-aside {
background-color: #545c64;
}
.el-header {
border-bottom: 1px solid #ccc;
line-height: 60px;
}
}
</style>
总结
路由V4对比V3细节变化
- 在vue3环境中,必须要使用vue-router(v4)
- 创建router实例的方式变了,使用 const router = createRouter({history, routes:[]})
- 指定路由模式的属性变了,使用history属性:createWebHashHistory() / createWebHistory()
- 路由注册,在mian.js中 app.use(router)
- 如果是选项式组件,this. r o u t e r / t h i s . router/this. router/this.route可以正常使用;如果是组合式API进行开发,必须使用 useRoute、userRouter等Hooks API进行开发。
- router-link已经没有tag属性的,可以用custom和插槽实现自定义渲染。
- router-view新增了"插槽"功能,极其强大,参见路由文档中的伪代码,它在实现keep-alive和transition动画将变得更简单,还可以Suspense实现Loading。
- 新增了几个组合API:useRoute / useRouter / useLink。
- 在V4中,淘汰了router.addRoutes(),只保留了 router.addRoute(),用于实现动态权限路由设计。
七、Pinia(V2)
Pinia使用
- Pinia(V2),是Vuex的下一版本,在Vue2和Vue3中都能用。
- Pinia支持多store开发,可以非常自由地组织"状态管理"的数据流逻辑。
- Pinia对Hooks编程思想非常友好,它对TS也非常友好。
第一步:安装
yarn add pinia
yarn add axios@0.27.2
第二步:创建并在main.ts中使用
// src/pages/student/store/useCnodeStore.ts
import { defineStore } from 'pinia'
import axios from 'axios'
// 强调:pinia要求定义store时,名称以use*开头的函数
const useCnodeStore = defineStore('cnode', {
// 这里的state,只能使用工厂函数写法
// 特点:在组件中可以被共享,并且具有响应式
state: () => {
return {
num: 1,
list: []
}
},
// 这里的getters就是计算属性,可用于计算state,还可以对另一个getters做计算
getters: {
total1 () {
return this.num * 10
},
total2 () {
return this.total1 * 2
}
},
// 这里的actions,可以直接修改state数据,无论是同步代码,还是异步代码。
actions: {
addNum (step=1) {
this.num += step
},
async getList (params) {
const res = await axios({
url: '/api/v1/topics',
method: 'GET',
params
})
if (res.status === 200) {
if (res.data && res.data.success) {
this.list = res.data.data
}
}
}
}
})
export default useCnodeStore
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
import router from './router'
import App from './App.vue'
let app = createApp(App)
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 注册路由
app.use(router)
app.use(ElementPlus)
// 创建pinia实例,注册app.use(pinia)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
在添加学生模块使用之,如下:
// src/pages/student/PageB.vue
<template>
<h1>状态管理</h1>
<h1 v-text="num"></h1>
<h1 v-text="store.total1"></h1>
<h1 v-text="store.total2"></h1>
<button @click="add">自增</button>
<button @click="reset">重置</button>
<hr />
<div v-for="(row, idx) in store.list" :key="row.id">
<span v-text="idx"></span>:
<span v-text="row.title"></span>
</div>
<button @click="pageHandle(-1)">上一页</button>
<button @click="pageHandle(1)">下一页</button>
</template>
<script setup>
import useCnodeStore from "./store/useCnodeStore";
import { storeToRefs } from "pinia";
import { onMounted, watchEffect, ref } from "vue";
const page = ref(1);
// store属性解构后,响应式失效,用storeToRefs包一层
const { num } = storeToRefs(useCnodeStore());
// store属性不解构,响应式没有任何问题。
const store = useCnodeStore();
// console.log('---store', store)
const add = () => {
// 修改pinia中的state数据【OK】
// store.$patch({num: store.num + 1})
// 使用actions方法修改pinia中的state数据
store.addNum(10);
};
const reset = () => {
store.$reset();
};
watchEffect(
() => {
store.getList({ page: page.value, limit: 5, tab: "ask" });
},
{ flush: "pre" }
);
const pageHandle = (num) => {
if (page.value === 1 && num === -1) return;
page.value += num;
};
watchEffect(() => {
// 监听store的变化
store.$subscribe((mutations, state) => {
// console.log('---mutations', mutations) // 信号
// console.log('---state', state) // store最新状态
// todo something
});
});
watchEffect(() => {
// 用于计算actions方法的执行时间
store.$onAction(({ name, after }) => {
const st = Date.now();
console.log(`${name} start`, st);
after(() => {
const et = Date.now();
console.log(`${name} end`, et);
console.log(`${name} 时间差`, et - st);
});
});
});
</script>
总结
- createPinia() 用于创建pinia实例
- defineStore(‘storeId’, { state, getters, actions }) 会定义store, 每个store都一个以use*开头的Hooks函数, state必须是工厂函数, getters不仅可以对state进行计算, 还可以另一个getters进行计算, actions不仅可以编写同步方法, 还可以编写异步逻辑.
- 怎么修改store中的state? 可以使用
$patch({})
进行批量修改, 还可以调用actions方法来修改. - 怎么重置store中的state? 使用
$reset()
方法重置store. - 如何监听store中的state的变化? 使用
$subscribe((mutations, state)=>{})
进行监听. 监听是一个副作用, 建议放在watchEffect中. - 如何测量actions方法的执行时间? 使用
$onAction(({name, after})=>{})
- 警告: 如果你对store进行解构, 响应式会失效, 使用 storeToRefs包裹一层即可.