Vue3相较于Vue2.x改动:
按照vue官方的说法,Vue3包含了以下性能方面的提升:
Composition API(组合API)
新的内置组件
其它改变
Vite是vue官方推荐的新一代的构建工具,和webpack有很大的区别:
优势(相较于传统的webpack等构建工具)
开发环境中,无需打包,可快速冷启动:
总所周知,用vue-cli创建的vue项目或者其他基于webpack构建工具的项目在运行时都需要一个打包的过程,打包后作为静态资源放在devServer创建的服务里才能访问查看我们写的页面,而vite构建的项目是可以快速冷启动而不需要打包的操作的。
轻量快速的热重载(HMR)
其实webpack同样支持热重载,只是vite的说法是更加轻量快速。
按需编译,不用等待整个应用编译完成:
下面是vite官网给到的webpack打包过程和vite冷启动过程原理以及流程
从上面的图里可以分析出来,webpack打包是从入口文件开始,分析项目的路由,再而分析路由所应用到的模块整体一起打包,然后启动devServer,然后我们才可以在浏览器访问本地服务端口去查看我们的工程界面。而vite则是用的是一个相反的思路进行开发环境的项目构建,首先就启动好服务然后分析入口文件,然后接下来并不会去打包所有路由所需依赖以及路由的组成模块,而是根据用户的访问去动态构建,因此vite构建的工程启动会快很多,而且就像是上面描述的:按需编译,不用等待整个应用编译完成。
使用vue-cli创建工程的过程和2.x版本差别不大,这里无须过多介绍。
和之前一样,main.js是vue项目的入口文件,只是在vue3.0中,创建vm的方法有很大的变化,在2.x中,是这样创建的
import Vue from "vue"
import App from "./App.vue"
import router from "./router"
import store from "./store"
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app")
这里就是引入构造函数Vue,然后new Vue传入配置项就生成了vm实例。在3.x中是这样的
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
这里使用vue库的createApp方法传入基组件生成vm,然后再调用vm的mount方法传入选择器将实例挂载在dom节点上。
provide和inject的作用就是解决父组件传值给后组件通过props传值需要一步一步往下传的痛点,provide提供的值可以在任意层级的后代中通过inject获取访问:
父组件:
<template>
<div>{{ "x is: " + x }}div>
<computed-child />
<button @click="increase">+button>
template>
<script>
import ComputedChild from "./ComputedChild.vue";
import { computed } from "vue";
export default {
components: { ComputedChild },
data() {
return {
x: 0,
y: 1,
};
},
provide() {
return { x: computed(() => this.x), y: computed(() => this.y), sum:computed(() => this.x + this.y) };
},
methods: {
increase() {
this.x++;
this.y++;
},
},
created() {
window.b = this;
},
};
script>
<style>style>
子组件:
<template>
computed-child
<div>{{ x }}div>
<div>{{ y }}div>
<div>{{ "sum is: " + sum.value }}div>
template>
<script>
export default {
inject: ["x", "y", "sum"],
};
script>
<style>style>
注意:provide可以是方法或者对象,如果你只提供常量那么用对象是完全可以的,但是当你需要提供组件实例身上的属性或者方法则需要将provide定义成一个方法,然后其返回值作为提供给后代组件的数据,还需注意的是:直接将组件的属性传递给后代这些属性不是响应式的,因此这个时候就需要借助vue3提供而组合式api computed了,用法如例子。
根据vue官网的介绍和自己的开发经验,当我们在做一些大型的项目或者组件的时候,通过data、computed、methods和watch等api来管理逻辑尽管通常都很有效很好用,然而如果我们的组件非常大的时候,逻辑变得更加复杂,代码会变得难以阅读和维护。这就是组合式API诞生的原因,目的就是把同一个逻辑关注点的代码收集在一起。
新的setup
选项会在组件创建之前进行,一旦props被解析,就将作为组合式API的入口。
注意:在setup中无法使用组件的this,因为就像上面说的,setup会在组件创建之前就进行,因此执行的时候是找不到组件实例的(因为还没有被创建),也就是说setup的调用发生在data、compited、和methods被解析之前。
setup是一个函数,接受两个参数(props和context),其中props就是组件的props:
基础用法如下:
// 父组件.vue
<template>
<HelloWorld :msg="msg" />
<button @click="updateMsg">修改button>
template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld,
},
data() {
return {
msg: "Welcome to Your Vue.js App",
};
},
methods: {
updateMsg() {
this.msg = "随机MSG" + Math.random() * 100;
},
},
};
script>
//子组件.vue
<template>
<div>
<div class="component">COMPONENTdiv>
<div>{{ "msg is: " + msg }}div>
<div>{{ "setup prop is: " + setupProp }}div>
div>
template>
<script>
export default {
props: ["msg"],
data() {
return {
data: "DATA",
};
},
setup(props, context) {
console.log(props, context);
return {
setupProp: props.msg,
};
},
mounted() {
window.a = this;
console.log("MOUNTED", this.setupProp);
},
};
script>
通过上面的例子可以发现setup返回的对象中的属性会被挂载在组件实例上,也就是说setup被调用后我们可以在组件实例的this身上访问到setup返回的对象的属性。(上面的例子就是在mounted钩子里面成功访问到了this.prop属性)。
需要注意的是:目前setup返回的对象的**非响应式
**的,因此在上面的例子中当我们通过按钮改变父组件中的msg的值的时候子组件中的setup是不会被重新调用的,setupProp的值也不会更新。接下来就要解决这个问题。
在vue3中我们可以通过新的api函数ref是任何响应式变量在任何地方起作用,ref接收参数并将其包裹在一个带有value属性的对象中返回,然后可以使用该property访问或者更改响应式变量的值。
注:这一步是很重要的,因为js中基本数据类型的传递是值的传递而引用类型的数据传递时引用的传递。因此将值封装为对象,我们就可以放心安全地在整个应用中去使用和传递而不必担心他丢失了响应性。
<template>
<div>
<div class="component">COMPONENTdiv>
<div>{{ "msg is: " + msg }}div>
<div>{{ "setup prop is: " + message }}div>
div>
template>
<script>
import { ref } from "vue";
export default {
props: ["msg"],
data() {
return {
data: "DATA",
};
},
setup(props, context) {
console.log(props, context);
const message = ref();
return {
message,
updateMsg() {
//注意ref定义的响应式变量在修改时需要修改其value属性
message.value = props.msg;
},
};
},
mounted() {
window.a = this;
this.updateMsg();
console.log("MOUNTED");
},
watch: {
msg: function() {
this.updateMsg();
},
},
};
script>
在这个例子中我们在setup中返回了message属性和updateMsg方法,然后在watch中监听msg这个prop的变化,一旦变化就调用updateMsg方法去更新message。
其实现在可以发现,我们之所以这样做其实就是为了解决上面说的逻辑多了难以维护的问题,现在我们就将message和message的处理方法updateMsg放在一个逻辑块中了。这使得我们的代码逻辑更加集中。
注意:ref封装定义的响应式对象在组件内访问的时候要用.value去取value属性才是他的真实值,因为ref包装后的数据位RefImpl对象,我们一般称为引用实现实例对象(一般简称 引用对象)
,但是在vue模板中不用去取value属性,vue模板编译器已经对其进行了解析。
const obj = reactive(originObj)
,返回一个新的代理对象obj,obj是响应式的,并且不同于ref函数,这里的obj不需要在通过value属性去获取其真实值用法:
<template>
<div>Reactivediv>
<p>{{ name }}p>
<p>Job is: {{ job.name }}p>
<p>Salary is: {{ job.salary }}p>
<p>level is: {{ job.level }}p>
<button @click="say">说话button>
<button @click="changeJob">换工作button>
<button @click="changeName">换名字button>
template>
<script>
import { reactive, ref } from "vue";
export default {
setup() {
let name = ref("zs");
let job = reactive({
level: 2,
salary: 20,
name: "Engineer",
});
const say = () =>
console.log(
`name is ${name.value} job is ${job.name} job level is ${job.level} job salary is ${job.salary}`
);
const changeJob = () => {
const newJob = {
level: 3,
salary: 30,
name: "Super Engineer",
};
Object.assign(job, newJob);
};
const changeName = () => {
name.value = "ls";
};
return {
name,
job,
say,
changeJob,
changeName,
};
},
};
script>
注意:就像上面提到的reactive函数定义基本数据类型的时候是无法实现响应式的,因此需要用ref函数定义名字,然而一般情况下我们不会两种都用,实际上我们在设计数据结构的时候就可以把全部需要的东西放在一个对象中。就可以不用ref函数只用reactive函数。
修改后:
<template>
<div>Reactivediv>
<p>{{ person.name }}p>
<p>Job is: {{ person.job.name }}p>
<p>Salary is: {{ person.job.salary }}p>
<p>level is: {{ person.job.level }}p>
<button @click="say">说话button>
<button @click="changeJob">换工作button>
<button @click="changeName">换名字button>
template>
<script>
import { reactive } from "vue";
export default {
setup() {
let person = reactive({
name: "zs",
job: {
level: 2,
salary: 20,
name: "Engineer",
},
});
const say = () =>
console.log(
`name is ${person.name} job is ${person.job.name} job level is ${person.job.level} job salary is ${person.job.salary}`
);
const changeJob = () => {
const newJob = {
level: 3,
salary: 30,
name: "Super Engineer",
};
Object.assign(person.job, newJob);
};
const changeName = () => {
person.name = "ls";
};
return {
person,
say,
changeJob,
changeName,
};
},
};
script>
可以用来为源响应式对象上的某个 property 新创建一个 ref
。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref
。
<template>
<div>Reactivediv>
<p>{{ name }}p>
<p>Job is: {{ job.name }}p>
<p>Salary is: {{ job.salary }}p>
<p>level is: {{ job.level }}p>
<button @click="say">说话button>
<button @click="changeJob">换工作button>
<button @click="changeName">换名字button>
template>
<script>
import { reactive, toRefs } from "vue";
export default {
setup() {
let person = reactive({
name: "zs",
job: {
level: 2,
salary: 20,
name: "Engineer",
},
});
person.say = () =>
console.log(
`name is ${person.name} job is ${person.job.name} job level is ${person.job.level} job salary is ${person.job.salary}`
);
person.changeJob = () => {
const newJob = {
level: 3,
salary: 30,
name: "Super Engineer",
};
Object.assign(person.job, newJob);
};
person.changeName = () => {
person.name = "ls";
};
// return { ...person }
return { ...toRefs(person) };
},
};
script>
问题:
这里如果不是 return { ...toRefs(person) };
这样子返回而是直接结构返回(return { ...person }
)的话,person身上的基本数据类型将不是响应式的,因为扩展运算符复制引用数据类型是浅拷贝,person的name是基本数据类型,job是引用数据类型,因而该person的job在内存中指向的地址和setup返回的新对象指向的是同一个地址,因而person的job改变的时候setup返回的person同样会改变,**如果直接返回person(return person
)而不是扩展复制返回是没有这个问题的。**然而在实际项目开发中我们一个组件可能不只有person这一个数据逻辑,因而需要用到toRef或者toRefs:
<template>
<div>Reactivediv>
<p>{{ name }}p>
<p>Job is: {{ job.name }}p>
<p>Salary is: {{ job.salary }}p>
<p>level is: {{ job.level }}p>
<button @click="say">说话button>
<button @click="changeJob">换工作button>
<button @click="changeName">换名字button>
<hr />
<p>{{ animal.name }}p>
<button @click="animal.changeName">换名字button>
template>
<script>
/*eslint-disable */
import { reactive, toRefs } from "vue";
export default {
setup() {
let person = reactive({
name: "zs",
job: {
level: 2,
salary: 20,
name: "Engineer",
},
});
person.say = () =>
console.log(
`name is ${person.name} job is ${person.job.name} job level is ${person.job.level} job salary is ${person.job.salary}`
);
person.changeJob = () => {
const newJob = {
level: 3,
salary: 30,
name: "Super Engineer",
};
Object.assign(person.job, newJob);
};
person.changeName = () => {
person.name = "ls";
};
let animal = reactive({
name: "Dog",
changeName() {
this.name = "Koki";
},
});
// return { ...toRefs(person) };
// return { ...person };
// return person;
return {
...toRefs(person),
animal,
};
},
};
script>
为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup
中注册生命周期钩子的方法。这要归功于 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on
:即 mounted
看起来会像 onMounted
。
这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
因此上面我们在mounted钩子里面初始化message就可以放在setup的onMounted钩子里面。
setup(props) {
const message = ref();
const updateMsg = () => {
message.value = props.msg;
};
onMounted(updateMsg);
return {
message,
updateMsg,
};
}
我们可以从vue引入watch选项,用来监听一个响应式value的变化,他接受三个参数:
首先通过一个例子来快速了解watch的工作方式:
import { ref, onMounted, watch } from "vue";
const counter = ref(0);
watch(counter, (newValue, oldValue) => {
console.log("The new counter value is: " + counter.value, newValue, oldValue);
});
setInterval(() => {
counter.value = counter.value + 1;
}, 1000);
了解了他的工作方式之后我们就可以将我们之前的demo里面通过vue组件实例的watch侦听器实现的数据相应放在setup里面:
<script>
import { ref, onMounted, watch, toRefs } from "vue";
export default {
props: ["msg"],
data() {
return {
data: "DATA",
};
},
setup(props) {
const message = ref();
//这里需要使用toRefs方法将获取msg的响应式应用
const msgRef = toRefs(props).msg;
const updateMsg = () => {
console.log("in update");
message.value = props.msg;
};
onMounted(updateMsg);
watch(msgRef, updateMsg);
return {
message,
updateMsg,
};
},
mounted() {
window.a = this;
console.log("MOUNTED");
},
// watch: {
// msg: function() {
// this.updateMsg();
// },
// },
};
script>
这样除了mounted的时候初始化message,甚至监听props变化更改msg的逻辑代码也都放到了setup中,这样子用一个逻辑块放在一起是逻辑结构更加清晰。
watchEffect和react的useEffect有点类似却也和vue2.x的computed类似,他是一个函数参数是一个回调,在回调里面用到的响应式数据都会被自动监听
<template>
<h1>
当前鼠标点位为x:{{ x }} y: {{ y }} total: {{ total }} tenTimesTotal {{ tenTimesTotal }}
h1>
template>
<script>
import { reactive, onMounted, toRefs, computed, watchEffect } from "vue";
export default {
setup() {
let point = reactive({
x: 0,
y: 0,
});
point.total = computed(() => point.x + point.y);
const getPoint = ({ pageX, pageY }) => {
console.log([pageX, pageY]);
point.x = pageX;
point.y = pageY;
};
onMounted(() => {
document.addEventListener("click", getPoint);
});
watchEffect(() => {
point.tenTimesTotal = point.total * 10;
});
return {
...toRefs(point),
};
},
};
script>
<style>style>
与 ref
和 watch
类似,也可以使用从 Vue 导入的 computed
函数在 Vue 组件外部创建计算属性。下面是computed的用法举例:
import { ref, computed } from "vue";
const name = ref("Joey Tribiani");
const firstName = computed(() => name.value.split(" ")[0]);
console.log(firstName.value); // "Joey"
它的性质其实和组件的生命周期钩子computed是类似的,就是根据一个或多个响应式数据的值通过某种计算规则得到一个新的我们需要的结果,下面的例子是一个求和的案例,但是我们在setup中用了computed和watch+mounted两种方式来计算和为sum和sum1,可以看出很多时候使用计算属性是非常简便的:
<template>
computed-child
<div>{{ x }}div>
<div>{{ y }}div>
<div>{{ "sum is: " + sum }}div>
<div>{{ "sum1 is: " + sum1 }}div>
template>
<script>
import { computed, ref, toRefs, onMounted, watch } from "vue";
export default {
props: ["x", "y"],
setup(props) {
//计算属性求和
const sum = computed(() => props.x + props.y);
//一般求和
const { x, y } = toRefs(props);
const sum1 = ref(0);
const updateSum = () => {
sum1.value = props.x + props.y;
};
onMounted(updateSum);
watch([x, y], updateSum);
return {
sum,
sum1,
};
},
};
script>
利用上面的鼠标打点的例子(在watchEffect的demo里面)来自定义hook,以此来深入理解组合式api的含义,在这个例子中,我们的功能就是在鼠标点击页面的时候记录下鼠标点击的坐标,然后展示在页面上:
在工程目录下创建hooks文件夹爱然后创建打点的hook,和react中的hooks一样大家都习惯用use开头来命名hook函数,这里我们就创建usePoint.js这个hook,然后把一下代码搬移到hook中:
import { reactive, onMounted, toRefs, computed, watchEffect } from "vue";
export default function() {
let point = reactive({
x: 0,
y: 0,
});
point.total = computed(() => point.x + point.y);
const getPoint = ({ pageX, pageY }) => {
console.log([pageX, pageY]);
point.x = pageX;
point.y = pageY;
};
onMounted(() => {
document.addEventListener("click", getPoint);
});
watchEffect(() => {
point.tenTimesTotal = point.total * 10;
});
return {
...toRefs(point),
};
}
然后回到savePoint组件,我们把之前写的关于point的数据;殴全部删除,因为这些已经在usePoint里面了,因此现在要做的只是引入usePoin这个hook并且调用就ok了:
<template>
<h1>
当前鼠标点位为x:{{ x }} y: {{ y }} total: {{ total }} tenTimesTotal
{{ tenTimesTotal }}
h1>
template>
<script>
import usePoint from "../../hooks/usePoint";
export default {
setup() {
return usePoint();
},
};
script>
在set返回值中,你可以直接返回,当然大多数情况下你一个组件都超过一个数据逻辑,因此一般我们会扩展之后暴露:
因此比如我们现在又多了一个usePerson的hook函数,我们应该这样做:
组件:
<template>
<h1>
当前鼠标点位为x:{{ x }} y: {{ y }} total: {{ total }} tenTimesTotal
{{ tenTimesTotal }}, person的name为 {{ person.name }}
h1>
<button @click="person.changeName">改名字button>
template>
<script>
import usePoint from "../../hooks/usePoint";
import usePerson from "../../hooks/usePerson";
export default {
setup() {
return { ...usePoint(), person: usePerson() };
},
};
script>
usePerson:
import { reactive } from "vue";
export default function() {
let person = reactive({
name: "Zhang San",
changeName() {
person.name = "Li Si";
},
});
return person;
}
这样子在组件里面就可以直接使用多个钩子返回的数据(注意这里usePerson()的返回值我没有展开,目的是为了展示这种用法,在setUp中,返回值的person指向的内存地址和hook中的返回值person指向的是同一个地方,因此可以这样用,但是如果usePerson返回的是基本数据类型则不可以这样做了)
现在用了hook之后,我们可以更加深刻的理解组合式api
这个概念了,在衣蛾hook中我们可以利用vue提供的各种api包装我们需要的数据,统一管理。