VUE3.0初探

首先直接创建一个项目,勾选上vuex,router和css预编译

创建完之后我们发现vue3和vue2的目录结构没有太大的区别,main.js中的内容稍微有一些变化

1
2
3
4
5
6
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

createApp(App).use(store).use(router).mount('#app')

而在router/index.js中,之前选的是hash模式,所以显示的是

1
2
3
4
const router = createRouter({
history: createWebHashHistory(),
routes
})

而这个哈希模式在代码的头部导入

1
import { createRouter, createWebHashHistory } from 'vue-router'

我们只要将导入的模块改为createWebHistory并且将history属性也改为这个就能直接切换成history模式

而store则基本没有什么变化

然后我们打开App.vue,发现template里面的内容是这样的

1
2
3
4
5
6
7
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</template>

回想一下vue2,在有路由的时候,两者是包含在一个div里面的,因为vue2的模板中只能包含一个元素,但是vue3取消了这个设定

基本上所有vue2中可以使用的语法在vue3中都可以使用

组合式API

vue3相比于vue2最重要的一个改变就是组合式api的引入,之前vue的模式让我们将数据和方法或者计算属性放在不同的内容里(data,method,computed),这就会导致一个问题:相关联的数据和方法并不在一块,有的时候甚至相距甚远,为代码阅读和维护造成一些困扰,当然vue2中的混入mixins可以一定程度上缓解这个问题,而vue3中针对这个问题,给出了组合式api作为解决方案

setup组件选项

vue3提供的setup组件选项会在创建组件之前执行,充当合成api的入口

而在setup中我们通常用ref函数来创建响应式变量 (类似于vue2中的data)

以下是一个使用的小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="home">
<div @click="addCounter">{{counter}}</div>
</div>
</template>

<script>
import { ref } from "vue";

export default{
setup(){
let counter = ref(0)
let addCounter = () =>{
counter.value++
}
return {
addCounter,counter
}
}
}
</script>

有几个注意点

  1. ref需要从vue解构 import { ref } from "vue"
  2. 使用ref创建的变量时,在setup中需要用counter.value,而在模板中可以直接用
  3. 所有需要在模板中使用的方法或者函数需要return出来

当然这个ref也可以创建字符串,数组等

1
2
let arr = ref([])
let str = ref("tmp")

生命周期钩子

在setup中还可以使用生命周期钩子

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, onMounted } from "vue";
export default{
setup(){
let counter = ref(0)
let addCounter = () =>{
counter.value++
}
onMounted(()=>{
console.log("渲染完毕")
})
return {
addCounter,counter
}
}
}

这个onMounted等价于组件选项mounted,实测onMounted运行时间要遭遇mounted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ref, onMounted } from "vue";
export default{
setup(){
let counter = ref(0)
let addCounter = () =>{
counter.value++
}
onMounted(()=>{
console.log("渲染完毕1")
})
return {
addCounter,counter
}
},
mounted(){
console.log("渲染完毕2")
}
}

其余生命周期钩子也可以类似地使用,beforeCreate和created除外,因为vue3认为这两个生命周期钩子和setup的职能重复了,就不需要套娃了,在这两个钩子函数中写的任何代码都应该直接写在setup中

需要注意的是这些生命周期要从vue中解构

watch响应式更改

在setup中我们还可以使用watch,等价于组件选项watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, watch } from "vue";

export default{
setup(){
let counter = ref(0)
let addCounter = () =>{
counter.value++
}
watch(counter,(newvalue,oldvalue)=>{
console.log(oldvalue,newvalue)
})
return {
addCounter,counter
}
}
}

watch同样也需要从vue中解构

以上写法等价于vue2中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default{
setup(){
let counter = ref(0)
let addCounter = () =>{
counter.value++
}
return {
addCounter, counter
}
}
watch:{
counter(newvalue, oldvalue){
console.log(oldvalue,newvalue)
}
}
}

独立的计算属性

计算属性computed也存在对应的方法,同样也需要从vue中解构才能使用

直接看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, computed } from "vue";

export default {
setup() {
let counter = ref(0);
let addCounter = () => {
counter.value++;
};
let doubleCounter = computed(() => counter.value * 2);
return {
addCounter,
counter,
doubleCounter,
};
},
};

封装

我们发现因为数据,方法,监听之类的在setup中都可以独立地创建和使用,因此相关联的数据可以很方便地放在一起

我们还可以将相关的数据封装在一个js中使得看起来更简洁一些

比如刚才的代码功能,我们可以新建一个counterApi.js文件,实现刚才在setup中写的内容并return,然后将封装函数export出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, computed } from "vue";

function counterApi() {
let counter = ref(0);
let addCounter = () => {
counter.value++;
};
let doubleCounter = computed(() => counter.value * 2);
return {
addCounter,
counter,
doubleCounter,
};
}

export default counterApi

就可以在组件中这么去使用

1
2
3
4
5
6
7
8
9
10
11
import counterApi from './counterApi'
export default {
setup() {
let { addCounter, counter, doubleCounter } = counterApi();
return {
addCounter,
counter,
doubleCounter,
};
},
};

这就允许我们更清晰地分块去撰写代码,比如这样

1
2
3
4
5
6
7
8
9
10
11
let { addCounter, counter, doubleCounter } = counterApi();

let { showImg, img } = imgApi();

return{
addCounter,
counter,
doubleCounter,
showImg,
img
}

emmm,说白了,其实这就是一个写得更爽一点的升级版的mixin

setup的参数

props参数

在vue2中我们如果需要在组件标签中传入属性,则需要使用组件选项props

比如我们写一个HelloWorld组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<h1>{{ msg }}</h1>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>

然后在about页面去使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="about">
<HelloWorld msg="你好" />
</div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
name: 'About',
components: {
HelloWorld
}
}
</script>

我们就能把这个你好传到HelloWorld组件的props中

如果我们要对这个msg进行一定的操作,则可以在methods或者computed里面通过this.msg来使用这个参数,比方说我们写一个字符串翻转的函数然后显示出来,就可以对HelloWorld进行如下更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<h1>{{ msg }}</h1>
+ <h1>{{ reverseMsg }}</h1>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
+ computed:{
+ reverseMsg:function(){return this.msg.split('').reverse().join('')}
+ }
}
</script>

但是如果我们在使用vue3提供的setup时,内部是不允许使用this的,输出this会发现直接就是undefined,那么如果想要使用props,则需要通过参数引入

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import {ref} from "vue"
export default {
name: "HelloWorld",
props: {
msg: String,
},
setup(props) {
let reverseMsg = ref(props.msg.split("").reverse().join(""));
return { reverseMsg };
},
};
</script>
props解构

props是一个响应式参数,所以不能够直接通过es6的方法来解构,需要通过toRefs来将数据解构出来,解构之后的数据是ref格式的,所以要直接使用数据,则需要通过.value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// toRefs也需要导入
import { ref, toRefs } from "vue";
export default {
name: "HelloWorld",
props: {
msg: String,
},
setup(props) {
let { msg } = toRefs(props);
// 解构的msg是ref格式,需要用msg.value来获取其值
let reverseMsg = ref(msg.value.split("").reverse().join(""));
return { reverseMsg };
},
};
</script>

context参数

setup还有第二个参数context

context中主要包含了三块内容,attrs, slots和emit

你可以这样去使用context

1
2
3
4
5
6
7
export default {
setup(props, context) {
console.log(context.attrs)
console.log(context.slots)
console.log(context.emit)
}
}

也可以直接在参数里解构它 (根据需要将内容写在大括号里)

1
2
3
4
5
6
7
export default {
setup(props, { attrs, slots, emit }) {
console.log(context.attrs)
console.log(context.slots)
console.log(context.emit)
}
}
attrs

context中的attrs用于获取上下文信息,即props未接收的非响应式对象

比方说,在刚才那个about页面,我们这样使用这个HelloWorld组件

1
2
3
4
5
<template>
<div class="about">
<HelloWorld msg="你好" msg2="你好呀" msg3="nice to meet you"/>
</div>
</template>

msg属性显然被props接收了,但是这个msg2和msg3并没有,而我们可以在attrs中得到这个msg2和msg3

我们输出这个attrs,可以看到

也就是我们可以通过msg.attrs.msg2来得到这个你好呀

所有被props遗弃的小朋友都被attrs接受了

emit

之前我们在使用vue2的时候子组件向父组件传值是使用this.$emit的


但是刚也说过,在setup中this是被禁用的

所以我们直接用context中解构出的emit来实现传值

1
2
3
4
5
6
7
8
// 子组件中
setup(props, { attrs, slots, emit }) {
console.log(slots);
let { msg } = toRefs(props);
let reverseMsg = ref(msg.value.split("").reverse().join(""));
+ emit("childEvent", "往父组件发送的信息");
return { reverseMsg };
}
1
2
3
4
5
6
7
// 父组件中
setup() {
let childFn = (data) => {
console.log(data);
};
return { childFn };
}

然后我们在组件标签中将childFn绑到childEvent事件上就可以通过emit触发

1
2
3
4
5
6
7
8
9
<div class="about">
<HelloWorld
msg="你好"
msg2="你好呀"
msg3="nice to meet you"
@childEvent="childFn"
>你好世界</HelloWorld
>
</div>

响应式对象

我们通过ref可以创建一个响应式的变量,但是当这个变量是一个对象,里面的内容并非响应式,也就是说,如果我们有一个变量是一个书架对象,书架上有很多书,如果我们替换了某本书,是不会触发书架的改变,除非我们换了书架(对象地址改变),而响应式对象的意思就是这个对象中任意属性的改变都会使得对象发生对应的更新

响应式对象的创建方式如下

1
2
3
4
5
let bookshelf = reactive({
book1: "哈利波特与死亡圣器",
book2: "哈利波特与火焰杯",
book3: "哈利波特与魔法石"
})

提供与注入

provide/inject可以使得后代组件更方便地使用父组件的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
import {provide} from "vue"
export default{
provide:{
nation: "中国",
addr:"地球"
}
}
// 后代组件
import {inject} from "vue"
export default{
inject:['nation','addr']
}

后代组件的意思是不仅子组件可以使用,子组件的子组件也可以这样获取父组件提供的信息,无论组件的深度有多深

在setup中也可以实现提供和注入,方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
import {provide} from "vue"
setup(){
// key value
provide("国籍", "中国")
}
// 后代组件
import {inject} from "vue"
export default{
setup(){
let data = inject("国籍")
}
}

路由api

直接看示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useRouter,useRoute} from "vue-router"
export default{
setup(){
const router = useRouter()
const route = unRoute()
// 使用时直接@click即可
let gohome = ()=>{
console.log(route)
router.push("/?username=123")
}
return {gohome}
}
}

vuex使用

和路由类似,对应vuex,vue3则是使用useStore来避免了使用this的情况

1
2
3
4
5
6
7
8
9
10
11
12
import {useStore} from "vuex"
export default{
setup(){
const store = useStore()
return{
description: computed(()=>store.state.description),
changeFn:()=>{
store.commit('setDes',"发送的信息")
}
}
}
}