vue(3)

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

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,即为了逻辑解耦的封装。下面我给两组代码,说明这两种使用场景。

实践

  1. 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>
  1. 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>
  1. 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>
  1. 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>
  1. 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、 routerouter、$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移除了(注意 childrenparent还在)
  • new Vue({ data:{} }) 这个写法淘汰了,如果用到data一定要使用工厂函数。
  • 移除了 $on / $off / o n c e ( 注 意 once (注意 onceemit还在)
  • 在V3中,事件总线这种通信这种基于订阅发布模式的通信方案也不能用了。
  • 移除了全局过滤器(Vue.filter)、移除了局部过滤器 filters选项。
  • 在V3中,Vue这个构造函数不能用了,所以像V2中的那个全局API都不能了,所以Vue的原型链也不能用了。
  • 在V3中,不再支持修改键盘码了。
  • 移除了 l i s t e n e r s ( 注 意 listeners (注意 listenersattrs还在)
  • 移除了 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包裹一层即可.

八、todomvc

项目地址
https://gitee.com/lcb522/todomvc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值