前言
前些时日闲暇时想写个demo,这才惊觉vue3从发布到如今已经两年多,而我平常只是在工作中使用vue2的开发,不知不觉间已落后于主流,于是便去查阅资料进行了vue3的速学,并记录下了学习笔记分享在此,希望能帮助到一些和我一样需要快速了解上手vue3的同学。PS:本文只是浅显的记录了一些vue3的用法,具体深入还请自行翻阅官网文档与源码或相关博客。
文章导图
Vue3的新变化
根据官方文档的说法,Vue3相较于Vue2在性能方面有如下提升:
- 资源大小:打包体积大小减少41%
- 初次渲染速度:加快了55%速度
- 更新速度:加快了133%的速度
- 内存占用:内存占比减少了54%
那么Vue3是如何做到的?
1. diff算法优化:
vue2:每次变更都会对虚拟DOM进行全量对比。
vue3:新增PatchFlag(静态标记),在与上次虚拟Node进行对比时,只对比有静态标记的Node。
2. hoist Static静态提升
vue2:无论元素是否参与更新,每次都会重新创建再渲染。
vue3:对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用。
新增新特性与变化如下:
- 响应式原理:由defineProperty的数据劫持变更为Proxy代理
- 生命周期变更
- 新增Composition API(组合api)
响应式原理
Vue2中通过Object.defineProperty来把对象中的每一个属性转成setter,getter。并结合watcher(观察者)通过发布订阅的模式进行视图的更新。
vue2响应式:
<script>
Object.defineProperty(data,'count',{
get () {},
set() {}
})
</script>
- 浅谈实现原理
- 对象类型:通过Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)注意:新增和删除不属于读写无法触发get、set方法
- 数组类型:通过重写更新数组的一系列方法来实现拦截。
- 存在问题:
- 新增属性、删除属性,界面不会更新。(不会触发get、set的读写方法)
- 直接通过下标修改数组,界面不会自动更新。
vue3响应式
<script>
const p = new Proxy(person, {
// Proxy是通过get、set来实现响应式原理的,注意:不触发它俩也能改变值但那是代理的映射而不是响应式,不修改源对象person
get(target, prop) { // target:源数据,prop:读取的key
// return target[prop];
return Reflect.get(target, prop);
},
set(target, prop, value) { // target:源数据,prop:要修改的key,value:要修改的value
// target[prop] = value; // 没有这步代理数据会变更,但是源数据依旧是旧的
return Reflect.set(target, prop);
},
deleteProperty(target, prop) { // target:源数据,prop:要删除的key
// return delete target[prop]; // 删除成功返回true,删除失败返回false
return Reflect.deleteProperty(target, prop);
}
});
</script>
实现原理:
- 通过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写,属性值的添加、属性值的删除等
- 通过Reflect(反射):对源对象的属性进行操作。
- MDN文档中描述的Proxy与Reflect
Composition API(组合API)
组件化作为Vue的重要思想,它允许我们将页面中繁杂的业务逻辑进行抽离,组合成一个个独立的功能块(组件),使得页面的管理维护更加的方便。但是当业务逻辑逐渐复杂、代码逐渐增多之后,面临成百上千的组件时,那么组件的共享与复用就变得尤为重要。
而vue3有个重要的新特性就是这个Composition API(组合api),Vue3在Vue2原有的组件化思想上进一步优化,使相同的逻辑的代码可以更加优雅的整合在一起,使得代码逻辑更加有序便于查找,从而解决逻辑代码分散维护起来麻烦的问题。
那么Composition API到底怎么用呢?我们就先从setup这个大舞台开始讲起吧。
setup
setup是所有Composition API(组合API)表演的舞台,也就是在vue组件中我们使用Composition API的地方。相较于2.x中的OptionsAPI需要data、methods、computed、watch等一系列配置项进行组合完成对应的功能逻辑不同,setup它的内部可以直接将数据源、函数、计算属性、监听等逻辑写在一起,这大大的降低了维护成本,如下:
// Vue2.x
<template>
<div>
<p>姓名:{{ person.name }}</p>
<p>职业:{{ person.career }}</p>
<button @click="Berserk">狂化</button>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name: "史蛋蛋",
career: "法师"
}
};
},
methods: {
Berserk() {
this.person.career = "狂战士";
}
},
watch: {
person: function(newValue, oldValue) {
console.log(`因为狂化你的职业临时由${oldValue}变更为:`, newValue);
}
}
};
</script>
// Vue3.x
<template>
<p>姓名:{{ person.name }}</p>
<p>职业:{{ person.career }}</p>
<button @click="Berserk">狂化</button>
</template>
<script>
import { watch } from "vue";
export default {
name: "App",
setup() {
let person = {
name: "史蛋蛋",
career: "法师"
};
function Berserk() {
person.career = "狂战士";
console.log(`因为狂化你的职业临时变更为:${person.career}`);
}
// 返回一个对象
return {
person,
Berserk
};
}
};
</script>
从上面代码我们可以看的出,在setup中可以将数据与方法等代码写在一起,最终return返回一个供外部使用的对象,这样的优势也是显而易见的。
本图引用自知乎大佬-“葡萄zi” zhuanlan.zhihu.com/p/424960765 setup也可以返回一个渲染函数
<script>
import { h, reactive } from "vue";
export default {
name: "App",
setup() {
let person = {
name: "史蛋蛋",
career: "法师"
};
function Berserk() {
person.career = "狂战士";
console.log(`因为狂化你的职业临时变更为:${person.career}`);
}
return () => [
h("p", `姓名:${person.name}`),
h("p", `职业:${person.career}`),
h("button", { onClick: Berserk }, "狂化")
];
}
};
</script>
</script>
当然这么写其实是有问题的,因为此时的数据并不是被proxy代理过的响应式数据,所以对象数据的修改并不会触发视图的更新,这就要引出我们Vue3中重要的响应式方法ref与reactive了
关于setup的总结如下:
- setup有两种返回结果,可以是对象也可以是渲染函数
- setup函数是处于生命周期函数 beforeCreate 和 Created 两个钩子函数之前的函数,因此this指向undefined
- setup作为函数时将接受两个参数:props与context
- props:props是一个对象,包含了父组件传递给子组件的所有数据。注:要使用传递过来的数据仍需props配置项接收。
- context:context是一个对象,内部包含attrs(获取当前标签的所有属性既父组件传递的属性,但只接受props中未接受过的属性,如果已接受则该属性为undefined)、emit(传递给父组件需要使用该事件)、slot(插槽)
- setup不能是一个async函数,因为返回值不再是return的对象, 而是promise对象, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
- vue3向下兼容依旧可以使用vue2的data和methods,但不建议混写
ref与reactive
ref:这个ref不是模板中的ref,它的作用是将setup函数声明的基本数据类型的变量转换为响应式对象,对象包含且仅有一个value属性,使用时直接使用变量名即可不需要.value(在js中使用则需要通过value获取)。
<template>
<p>{{ name }}</p>
<button @click="name = '景天'">转世</button>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
let name = ref("李逍遥");
return {
name
};
}
};
</script>
可以看到此时通过ref声明的这个变量就已经是响应式了,数据的变更会触发视图的更新。这里推荐基本数据类型使用ref定义,那么对象这样的引用数据类型呢?
reactive:reactive可以将一个对象类型(数组也可)的数据转换为响应式。它将对象转换为一个proxy代理对象,通过代理对象操作源对象内部数据进行操作。
<template>
<p>姓名:{{ person.name }}</p>
<p>职业:{{ person.career }}</p>
<p>属性:{{ person.type }}</p>
<p>技能池:{{ person.sikll }}</p>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
let person = reactive({
name: "史蛋蛋",
career: "法师",
type: ["地", "火", "水", "风", "光", "暗"],
sikll: {
q: "杰克爆弹",
w: "断崖之剑",
e: "根源波动",
r: "圣光术"
}
});
return {
person
};
}
};
</script>
总结:
- ref:定义基本数据类型的响应式数据,虽然也可以定义对象但其实是内部调用了reactive,所以并不推荐。
- reactive:定义一个对象类型的响应式数据,reactive定义的响应式数据是深层次的,内部数据都是响应式。
computed(计算属性)
与Vue2中的computed配置功能一致,依旧可以用原有写法,但不推荐。
<script>
import { computed, reactive } from "vue";
setup() {
let person = reactive({
lastName: "李",
firstName: "逍遥",
maxATK: 100,
minATK: 20,
ATK: 50
});
// 简写
let fullName = computed(() => {
return person.lastName + person.firstName;
});
person.fullName = fullName;
// 完整
let meanATK = computed({
get() {
return (person.maxATK + person.minATK) / 2;
},
set(value) {
console.log(value);
person.ATK = value;
return person.ATK;
}
});
person.meanATK = meanATK;
// 返回一个对象
return {
person
};
// 返回一个函数(渲染函数)
// return () => h("h1", "我是渲染函数");
}
</script>
watch监听侦查器
watch的用法与Vue2中的watch选项用法相同,注意以下几点即可
- 监听reactive定义的响应式数据中的某个对象属性时,需要开启deep配置,否则无法深度监听属性变化
- ref定义的对象其value会隐式调用reactive方法,所以想监听器其变化需要,监听ref定义对象的value或者开启深度监听
<template>
<h1>{{ person.name }}的属性面板</h1>
当前等级:{{ minLevel }}
<button @click="minLevel = maxLevel">升级</button>
<button @click="person.ATK += 2000">使用{{ person.gameSkills.w }}</button>
<br />
<button @click="shapeshift">我不做人了,JOJO!</button>
<br />
<button @click="person.gameSkills.f = '李逍遥召唤了战宠裂空座'">
通灵之术!
</button>
<br />
<button @click="person.gameSkills.r = '御剑降魔'">绝技</button>
<input type="text" v-model="role.name" />
<br />
<button>转世</button>
</template>
<script>
import { ref, watch, reactive } from "vue";
export default {
name: "App",
// 此处只是测试一下setup,暂时不考虑响应式的问题
setup() {
let minLevel = ref(10);
let maxLevel = ref(100);
let person = reactive({
name: "李逍遥",
ATK: 10086,
gameSkills: {
q: "御剑术",
w: "酒神咒",
e: "飞龙探云手",
r: "万剑诀"
}
});
let role = ref("景天");
console.log(person);
// 监听一个ref定义的基本类型的响应式数据
watch(minLevel, (newValue, oldValue) => {
console.log("恭喜您的等级由", oldValue, "提升到了", newValue); // 初始攻击力:100 增强后的攻击力:1001
});
// 监听多个ref定义的基本数据类型
watch(
[maxLevel, minLevel],
(newValue, oldValue) => {
console.log(
"初始伤害范围:" + oldValue,
"增强后的伤害范围:" + newValue
); // 初始伤害范围:100,10 增强后的伤害范围:1001,10
},
{ immediate: true }
);
/*
* 注1:获取的oldValue与newValue值相同,解决办法暂不得知
* 注2:深度监听强制开启,无法通过deep关闭
*/
watch(
person,
newObj => {
console.log("由于酒神咒的效果攻击力暂时上涨到了:", newObj.ATK);
console.log(
"当前可使用的技能如下",
"技能1-",
newObj.gameSkills.q,
"技能2-",
newObj.gameSkills.w,
"技能3-",
newObj.gameSkills.e
);
},
{ deep: false }
);
// 监听reactive定义的某个对象。deep: false 时只监听当前对象,对象内的属性变动不会触发监听时间
watch(
() => person.gameSkills,
(newSkills, oldSkills) => {
console.log("李逍遥的技能池发生了变化:", oldSkills, "=>", newSkills);
},
{ deep: true }
);
// 监听reactive定义的某个对象中的属性
watch(
() => person.gameSkills.r,
(newSkills, oldSkills) => {
console.log("李逍遥的:", oldSkills, "暂时进阶为:", newSkills);
},
{ deep: false }
);
// 监听ref定义的某个对象。ref定义的对象其value会隐式调用reactive方法,所以想监听器其变化需要,监听ref定义对象的value或者开启深度监听
watch(
role.value,
newSkills => {
console.log("我是新角色:", newSkills);
},
{ deep: true }
);
function shapeshift() {
const gameSkills = {
q: "拔刀斩",
w: "蓄意轰拳",
e: "神罗天征",
r: "TheWrold!"
};
person.gameSkills = gameSkills;
}
// 返回一个对象
return {
minLevel,
maxLevel,
person,
role,
shapeshift
};
}
};
</script>
watchEffect
对了,watch还有一个新方法watchEffect,它的作用是在watchEffect内部使用过的数据在变更时会触发监听,既只注重过程而非结果。
<script>
watchEffect(() => {
const q = person.gameSkills.q;
const f = person.gameSkills.f;
const w = person.w;
console.log("李逍遥使用了技能");
});
</script>
watch、watchEffect、computed三者的区别
-
与watch的区别
- watchEffect 不需要指定监听的属性,他会自动的收集依赖, 监听的回调中使用了那个属性就监听那个,而 watch 只能监听指定的属性而做出变更。
- watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的
- watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。
-
与computed的区别
- computed注重计算的结果,需要写返回值。
- watchEffect注重计算过秤,不用写返回值。
toRef与toRefs
在平时的开发中,我们会遇到将对象内部的属性提供给外部使用的情况,那么就可能会存在一种问题,像是把对象中的一个非引用类型数据暴露出去,会使得的它失去响应式,这时候我们就需要将一个完整的引用数据类型对象进行返回,无形中增加了工作量。那么怎么解决这个问题呢?Vue3给了我们提供了两种方法toRef与toRefs。
- toRef:创建一个ref对象,其value值指向另一个对象中的某个属性。
- toRefs:功能一致,toRefs可以批量创建多个ref对象。
用法如下:
<template>
<div>
<p>姓名:{{ name }}</p>
<p>性别:{{ age }}</p>
<p>宠物:{{ pokemoName }}---技能:{{ gameSkills }}</p>
</div>
</template>
<script>
import { reactive, toRef, toRefs } from "@vue/reactivity";
export default {
setup() {
let person = reactive({
name: "李逍遥",
age: "男",
gameSkills: {
q: "御剑术"
}
});
let pokemon = reactive({
pokemoName: "固拉多",
gameSkills: "断崖之剑"
});
const p = toRefs(pokemon); // 将整个对象内部属性变为可以暴露在外部的响应式数据
return {
name: person.name, // 字符串而非响应式
age: toRef(person, "age"), // 响应式
...p // 解构返回
};
}
};
</script>
可以看到通过toRef返回的基本数据类型,此时已经由字符串变为了响应式数据,而toRefs定义的对象内部也全变为了响应式,并且通过解构赋值返回后可以更加方便的使用。
除了这些外Vue3还提供了一些其他的API
Composition API 拓展
shallowReactive与shallowRef
- shallowReactive:只处理对象最外层属性的数据使其变成响应式。
- shallowRef:只处理基本类型的响应式使其变成响应式,不对对象进行响应式处理。
- 语法:shallowReactive(Object);shallowRef(value);
readonly 与shallowReadonly
- readonly:让一个响应式数据变为只读(深知读)
- shallowReadonly:让一个响应式数据的最外层数据变为只读(浅只读)
toRaw与markRaw
- toRaw:将一个由reactive生成的响应式对象转为普通对象。
- markRaw:标记一个对象,使其永远不会再成为响应式对象。
customRef
- 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track(追踪) 和 trigger(触发响应) 函数作为参数,并且应该返回一个带有 get 和 set 的对象
- 应用场景:防抖函数。
<template>
<div>
<input type="text" v-model="text" />
<h1>{{ text }}</h1>
</div>
</template>
<script>
import { customRef } from "@vue/reactivity";
export default {
setup() {
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
};
});
}
return {
text: useDebouncedRef("hello")
};
}
};
</script>
provide与inject
provide与inject,都是用于父级组件与子代组件进行通信的方法。相较于props省去了多层级之间的数据传递。并且可以改变父组件传递的数据。
- provide:父级组件使用provide向下进行传递数据。
- inject:子级组件使用inject来获取上级组件传递过来的数据。
// 最高层级的组件
<template>
<div class="box">
<h1>
我是祖先:mage-绿毛虫----持有物:{{ pokemon.mage }}
<parent />
</h1>
</div>
</template>
<script>
import { provide, reactive } from "@vue/runtime-core";
import parent from "./components/parent.vue";
export default {
name: "App",
components: { parent },
setup() {
let pokemon = reactive({
redOrb: "朱红宝珠",
blueOrb: "靛蓝宝珠",
mage: "画龙点睛"
});
provide("pokemon", pokemon);
return {
pokemon
};
}
};
</script>
// 父组件
<template>
<div class="parent-box">
<h2>
我是父组件:盖欧卡 ---- 持有物:{{ orb.blueOrb }}
<children></children>
</h2>
</div>
</template>
<script>
import { inject } from "@vue/runtime-core";
import children from "./children.vue";
export default {
components: { children },
setup() {
const orb = inject("pokemon");
return {
orb
};
}
};
</script>
// 后代子组件
<template>
<div class="children-box">
<h3>我是后代组件:固拉多---持有物:{{ orb.redOrb }}</h3>
</div>
</template>
<script>
import { inject } from "@vue/runtime-core";
export default {
setup() {
const orb = inject("pokemon");
return {
orb
};
}
};
</script>
响应式数据的判断
还有一些新的判断方式
- isRef:检查一个值是否为ref对象。
- isReactive:检查一个对象是否是由reactive创建的响应式代理。
- isReadonly:检查一个对象是否是由readonly创建的只读代理。
- isProxy:检查一个对象是否是由reactive或者readonly方法创建的代理。
自定义hook函数
当然了既然是组合API那么自然也是支持封装提取的,hook函数就是用来做这种事的。
什么是hook?本质上是一个函数,类似于Vue2中的mixin,将setup函数中使用的CompositionAPI进行了封装。使代码可以复用,让setup中的逻辑更清楚易懂。
用法如下:
<template>
<button v-show="!pokemon.combat" @click="goRayquaza">召唤</button>
<button v-show="pokemon.combat" @click="pokemon.combat = false">收回</button>
<p v-show="pokemon.combat">属性如下:{{ pokemon }}</p>
</template>
<script>
import { ref } from "vue";
import pokeBall from "./hook/pokeBall";
export default {
name: "App",
setup() {
let pokemon = ref({});
function goRayquaza() {
let pokeBallObj = pokeBall();
console.log(pokeBallObj);
pokeBallObj.pokemon.combat = true;
pokemon.value = pokeBallObj.pokemon;
}
// 返回一个对象
return {
pokemon,
goRayquaza
};
}
};
</script>
新的组件
Fragment
Vue3还新增了一些新的组件,可能有些人在看到之前写的代码时会有一个疑惑,怎么我的template中没有根元素,那是因为在3.x中新增了一个Fragment标签,Vue3会在template中默认生成一个Fragment虚拟根标签将多个标签元素包含在其中,这样一来自然便不需要再专门去写一个根标签,优化了代码结构。
Teleport
在处理嵌套结构的组件时,例如弹出框的定位与样式之类会比较麻烦,而Teleport就可以帮我们方便的解决组件间 css 层级问题。
Teleport:是一种能够将组件内部的HTML结构移动到指定位置的技术。
// 父组件
<template>
<div class="box" id="sky">
<h1>
<p>这里是天空</p>
我是父组件:mage-绿毛虫----持有物:{{ pokemon.mage }}
<parent />
</h1>
</div>
</template>
<script>
import { provide, reactive } from "@vue/runtime-core";
import parent from "./components/parent.vue"; // 静态引入
export default {
name: "App",
components: { parent },
setup() {
let pokemon = reactive({
redOrb: "朱红宝珠",
blueOrb: "靛蓝宝珠",
mage: "画龙点睛"
});
provide("pokemon", pokemon);
return {
pokemon
};
}
};
</script>
// 子组件
<template>
<div class="parent-box">
<h2>
<p>这里是海洋</p>
<teleport to=".box">
我是子组件:盖欧卡 ---- 持有物:{{ orb.blueOrb }}
<img src="../assets/gok.png" />
</teleport>
<children></children>
</h2>
</div>
</template>
<script>
import { inject } from "@vue/runtime-core";
import children from "./children.vue";
export default {
components: { children },
setup() {
const orb = inject("pokemon");
return {
orb
};
}
};
</script>
<style>
.parent-box {
background-color: aqua;
}
img {
width: 220px;
height: 200px;
}
</style>
// 孙组件
<template>
<div class="children-box">
<h3>这里是陆地:</h3>
<div>
<h3>我是孙组件:固拉多---持有物:{{ orb.redOrb }}</h3>
</div>
</div>
</template>
<script>
import { inject } from "@vue/runtime-core";
export default {
setup() {
const orb = inject("pokemon");
function btn() {
orb.redOrb = "a";
}
return {
orb,
btn
};
}
};
</script>
<style>
.children-box {
background-color: red;
}
</style>
从代码中我们可以看到原本应当在子组件海洋中的盖欧卡被传送到了父组件的天空之中,这就是Teleport的作用,将任意组件传送到指定容器组件的位置当中。
Supense
还有Supense,它可以在等待异步组件时进行一些渲染处理,提升用户的使用体验。
使用方法:
<template>
<div class="box" id="sky">
<h1>
<p>这里是天空</p>
我是祖先:mage-绿毛虫----持有物:{{ pokemon.mage }}
<Suspense>
<!-- 异步加载成功时 -->
<template v-slot:default>
<parent />
</template>
<!-- 异步加载中的显示 -->
<template v-slot:fallback>
不会飞正在返回芳缘的路上...
</template>
</Suspense>
</h1>
</div>
</template>
<script>
// import parent from "./components/parent.vue"; // 静态引入
import { defineAsyncComponent } from "vue";
const parent = defineAsyncComponent(() => import("./components/parent.vue")); // 异步引入
export default {
name: "App",
components: { parent },
};
</script>
生命周期
最后的最后,我们再来看看Vue3生命周期的变化。
3.x依旧可以沿用2.x中的生命周期钩子,但需要注意的是有两个钩子变更了。
-
beforeDestroy => beforeUnmount
-
destroyed => unmouted Vue3还新提供了Composition API(组合api)形式的生命周期钩子,你可以在setup()中通过在生命周期钩子前面加上 “on” 来访问它们。
-
beforeCreate => setup()
-
created => setup()
-
beforeMount => onBeforeMount
-
mounted => onMounted
-
beforeUpdate => onBeforeUpdate
-
updated => onUpdated
-
beforeUnmount => onBeforeUnmount
-
unmounted => onUnmounted
-
errorCaptured => onErrorCaptured
-
renderTracked => onRenderTracked
-
renderTriggered => onRenderTriggered
-
activated => onActivated
-
deactivated => onDeactivated
-
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,setup()就等于beforeCreate 和 created。
结束
以上就是Vue3速读上手的全部内容,感谢各位开发者与分享者,咱们山水有相逢。嘻~