七、Vue3
1、创建Vue3工程
(1)使用vue-cli创建
输入命令vue create vue3_test即可创建名为vue3_test的项目
(2)使用vite创建
输入命令npm init vite-app vue3_test_vite即可创建名为vue3_test_vite的项目
注意: 使用vite创建的项目没有安装依赖,需要后续手动安装。
2、Vue3工程结构
(1)main.js文件
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
在Vue3中不再引入Vue构造函数,而是引入一个名为createApp的工厂函数
通过create(App)会创建应用实例对象,类似于Vue2中的vm,但是会比vm更轻,其内部结构如下所示:
(2)App.vue文件
在Vue3的模板中不再需要将结构写在一个根标签中了
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
3、常用的Composition API
(1)setup
setup是Vue3中的一个新的配置项,值为一个函数。为所有Composition API提供了基础。组件中用到的数据、方法等,都要配置在setup中。
setup函数的两种返回值
-
返回对象
如果返回一个对象,则对象中的属性和方法在模板中都可以直接使用
<template> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> <button @click="sayHello">点我说话</button> </template> <script> export default { name: 'App', setup() { let name = "dexter"; let age = 18; function sayHello() { alert(`我叫${name},我${age}岁了`) } return { name, age, sayHello } } } </script> -
返回渲染函数
如果返回一个渲染函数,则可以自定义渲染的内容
<template> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> </template> <script> import { h } from "vue"; export default { name: 'App', setup() { let name = "dexter"; let age = 18; return () => h("h2", "hello") } } </script>备注: 如果返回了一个渲染函数,则渲染函数中的内容会替换掉模板中的内容。
注意:
- 尽量不要与Vue2.x配置混用。如果混用,则Vue2.x的配置(data、methods、computed...)中可以访问到
setup中的属性和方法,但是setup中不能访问到Vue2.x中的配置;如果两者在命名上出现了冲突,会优先使用setup中的配置。setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性了。(后期也可以返回一个Promise实例,但是需要Suspense和异步组件的配合)
(2)ref函数
ref函数的作用是定义一个响应式的数据。
语法const xxx = ref(initValue),创建一个包含响应式的引用对象,使用js操作数据时要使用xxx.value来获取,但是在模板中读取数据不需要.value,直接使用原始数据即可。
备注: ref接收的数据可以是基本类型、也可以是对象类型,基本数据类型的响应式依然是靠Object.defineProperty()的get和set实现的,所以在获取数据时需要使用.value;而对象类型的数据内部求助了Vue3.0中的一个新的函数reactive,所以在使用时只需要写一次.value即可获取到对应的数据,对于对象内部的数据,不需要层层.value获取。
<template>
<h1>hello</h1>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>工作:{{job.type}},薪资{{job.salary}}</h2>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import { ref } from "vue";
export default {
name: 'App',
setup() {
let name = ref("dexter");
let age = ref(18);
let job = ref({ type: "front end", salary: "30K" });
function changeInfo() {
name.value = "emma";
job.value.type = "java";
console.log(name, age, job);
}
return { name, age, job, changeInfo }
}
}
</script>

(3)reactive函数
reactive函数的作用是定义一个对象类型的响应式数据(基本数据类型不能使用reactive,要使用ref),语法const 代理对象 = reactive(源对象),reactive函数接收一个对象或数组,返回一个代理对象(proxy对象)。
reactive定义的响应式数据是深层次的,内部基于ES6的Proxy实现,通过代理对象操作源对象内部数据。
let job = reactive({
type: "front end",
salary: "30K"
});
function changeInfo() {
name.value = "emma";
job.type = "java";
}
在changeInfo中输出job可以看到其就是一个Proxy对象
(4)Vue3的响应式原理
Vue2中存在的问题是:新增属性、删除属性,界面不会更新;直接通过下标修改数组时,界面不会更新。必须通过Vue提供的$set、$delete等方法才能够操作数据。
Vue3中通过reactive函数解决了上述问题,将对象写在该函数中,后续的任意操作都可以正常显示。
原生Proxy的使用
let person = {
name: "dexter",
age: 18
};
let p = new Proxy(person, {
//当有人读取了p身上的某个属性时调用
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`);
return target[propName]
},
//当有人修改p的某个属性、或者给p追加某个属性时调用
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性`);
target[propName] = value;
},
//当有人删除了p身上的某个属性时调用
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性`);
return delete target[propName]
}
});
通过Proxy代理,拦截对象中任意属性变化,包括属性的读写,属性的添加和删除等。
reflect是一个js内置的对象,它提供拦截js操作的方法。静态方法Reflect.defineProperty()基本等同于 Object.defineProperty() 方法,Object.defineProperty方法如果成功则返回一个对象,否则抛出一个 TypeError 。另外,当定义一个属性时,你也可以使用 try...catch去捕获其中任何的错误。而因为 Reflect.defineProperty返回 Boolean 值作为成功的标识,所以只能使用 if...else:
let person = {
name: "dexter",
age: 18
};
const x = Reflect.defineProperty(person, "sex", {
value: "male"
});
if(x) {
console.log("添加成功");
}else {
console.log("添加失败");
}
vue3中的响应式如下所示:
let person = {
name: "dexter",
age: 18
};
let p = new Proxy(person, {
//当有人读取了p身上的某个属性时调用
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`);
return Reflect.get(target, propName)
},
//当有人修改p的某个属性、或者给p追加某个属性时调用
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性`);
return Reflect.set(target, propName, value)
},
//当有人删除了p身上的某个属性时调用
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性`);
return Reflect.deleteProperty(target, propName)
}
});
在控制台中的操作展示
(5)reactive与ref的对比
-
从定义数据角度对比
- ref用来定义基本数据类型(ref也可以用来定义对象或数组类型数据,但是它内部会自动通过reactive转化为代理对象)
- reactive用来定义对象或数组类型数据
-
从原理角度对比
- ref通过
Object.defineProperty()的get和set来实现响应式(数据劫持) - reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据
- ref通过
-
从使用角度对比
- ref定义的数据操作时需要使用
.value,读取数据时模板中可以直接读取 - reactive定义的数据操作与读取数据都不需要
.value
- ref定义的数据操作时需要使用
(6)setup的两个注意点
-
执行时机
setup会在beforeCreate生命周期执行之前执行一次;setup的this是undefined -
参数
-
props
值为对象,包含组件外部传递过来且组件内部声明接收了的属性
-
context
上下文对象
attrs的值为对象,包含组件传递过来但是没有在props配置中声明的属性,相当于vue2中的this.$attrsslots表示收到的插槽内容,相当于vue2中的this.$slotsemit是分发自定义事件的函数,相当于vue2中的this.$emit
-
父组件编码:
<template>
<Demo @hello="showHelloMsg" msg="hello" school="hebeu">
<template v-slot:nihao>
<span>hello</span>
</template>
</Demo>
</template>
<script>
import Demo from "./components/Demo.vue";
export default {
name: 'App',
components: {Demo},
setup() {
function showHelloMsg(value) {
console.log(`收到了参数${value}`);
}
return {showHelloMsg}
}
}
</script>
子组件编码
<template>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<button @click="test">触发自定义事件</button>
</template>
<script>
import { reactive } from "vue";
export default {
name: "Demo",
props: ["msg", "school"],
emits: ["hello"],
setup(props, context) {
let person = reactive({
name: "dexter",
age: 18
});
function test() {
context.emit("hello", "how are you")
}
return { person, test }
}
}
</script>
(7)计算属性
-
computed函数
与vue2中的computed函数的配置功能一致,但是需要提前引入
import { reactive, computed } from "vue"; export default { name: "Demo", setup() { let person = reactive({ firstName: "dexter", lastName: "jun" }); person.fullName = computed({ get() { return person.firstName + "-" + person.lastName; }, set(value) { person.firstName = value.split("-")[0]; person.lastName = value.split("-")[1]; } }); return { person } } }如果只需要读取数据,可以使用简写形式
person.fullName = computed(() => { return person.firstName + "-" + person.lastName; }); -
watch函数
在
setup中定义了如下数据let sum = ref(0); let msg = ref("hello"); let person = reactive({ name: "dexter", age: 18, job: { j1: { salary: 20 } } });-
情况一:监视ref所定义的一个响应式数据
watch(sum, (newValue, oldValue) => { console.log("sum的值发生变化了", newValue, oldValue); })数据改变时控制台输出:
-
情况二:监视ref所定义的多个响应式数据
watch([sum, msg], (newValue, oldValue) => { console.log("sum或msg发生变化了", newValue, oldValue); }, {immediate: true})数据改变时控制台输出:
-
情况三:监视reactive所定义的一个响应式数据
watch(person, (newValue, oldValue) => { console.log("person发生变化了", newValue, oldValue); })当person中的数据发生改变时
watch会监测到,但是值得注意的是这里只能监测到newValue的值,并不能监测到oldValue的值;且默认强制开启了深度监视,即使配置deep: false也没用,数据变化时控制台输出: -
情况四:监视reactive所定义的一个响应式数据中的某个属性
watch(() => person.job.j1.salary, (newValue, oldValue) => { console.log("薪资变化了", newValue, oldValue); })数据变化时控制台输出:
-
情况五:监视reactive所定义的一个响应式数据中的多个属性
watch([() => person.name, () => person.job.j1.salary], (newValue, oldValue) => { console.log("薪资变化了", newValue, oldValue); })数据变化时控制台输出:
-
特殊情况
watch(() => person.job, (newValue, oldValue) => { console.log("job变化了", newValue, oldValue); }, {deep: true})此处由于监视的是reactive所定义的对象中的某个属性,所以deep配置有效,数据改变时控制台输出:
-
关于
.value的问题setup() { let sum = ref(0); let msg = ref("hello"); let person = ref({ name: "dexter", age: 18, job: { j1: { salary: 20 } } }); console.log(sum); console.log(person); watch(sum, (newValue, oldValue) => { console.log("sum发生变化了", newValue, oldValue); }) watch(person.value, (newValue, oldValue) => { console.log("person发生变化了", newValue, oldValue); }) }从下图中可以看到通过
ref所定义的数据会被包装成一个RefImpl对象,基本数据类型会直接作为value属性的值存储,引用数据类型会被reactive包装成一个Proxy对象再交给value保存。因此在使用watch函数监视ref绑定的基本数据类型时不需要使用.value,而监视引用数据类型时需要.value。备注: 如果不想使用
.value可以不写,但是要为watch配置深度监视。
-
-
watchEffect函数
在编写
watch函数时,既要指明监视的属性,又要指明监视的回调;而在编写watchEffect时,不用指明监视哪个属性,监视的回调中用到哪个属性,就会自动监视哪个属性,写法如下:watchEffect(() => { msg.value = "emma"; let x = person.name; console.log("watchEffect执行了"); })备注:
watchEffect会在组件挂载后执行一次。watchEffect和computed的比较:相同点:二者都是根据依赖数据的变化而执行回调函数的
不同点:
computed注重的是计算出来的值(回调函数的返回值),所以必须要写返回值;而watchEffect更注重的是过程(回调函数的主体),所以不用写返回值。
(8)Vue3生命周期
vue3中可以继续使用vue2中的生命周期钩子,但是有两个被更名:beforeDestroy更改为beforeUnmount;destroyed更改为unmounted
Vue3中也提供了Componsition API形式的生命周期钩子,与Vue2中钩子的对应如下:
beforeCreate()======>setup()created()======>setup()beforeMount()======>onBeforeMount()mounted()======>onMounted()beforeUpdate()======>onBeforeUpdate()updated()======>onUpdated()beforeUnmount()==>onBeforeUnmount()unmounted()=====>onUnmounted()
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from "vue";
setup() {
console.log("setup");
let sum = ref(0);
onBeforeMount(() => {
console.log("onBeforeMount");
})
onMounted(() => {
console.log("onMounted");
})
onBeforeUpdate(() => {
console.log("onBeforeUpdate");
})
onUpdated(() => {
console.log("onUpdated");
})
onBeforeUnmount(() => {
console.log("onBeforeUnmount");
})
onUnmounted(() => {
console.log("onUnmounted");
})
return {sum}
},
(9)自定义hook函数
自定义hook的本质是一个函数,函数中把setup函数中的composition API进行了封装,类似于vue2中的mixin。自定义hook的优势是复用代码,让setup中的逻辑更加清晰。
一般将hook函数定义在src/hooks文件夹下,定义usePoint.js文件如下:
import { onBeforeUnmount, onMounted, reactive } from "vue";
export default function() {
let point = reactive({
x: 0,
y: 0
});
function savePoint(event) {
point.x = event.pageX;
point.y = event.pageY;
console.log(point.x, point.y);
}
onMounted(() => {
window.addEventListener("click", savePoint)
})
onBeforeUnmount(() => {
window.removeEventListener("click", savePoint)
})
return point;
}
在组件中使用自定义hook
<template>
<h2>当前求和为:{{sum}}</h2>
<button @click="sum++">点我+1</button><hr>
<h2>当前鼠标点击的坐标为:x:{{point.x}},y:{{point.y}}</h2>
</template>
<script>
import { ref } from "vue";
import usePoint from "../hooks/usePoint";
export default {
name: "Demo",
setup() {
let sum = ref(0);
let point = usePoint();
return { sum, point }
}
}
</script>
(10)toRef
通过调用toRef来创建一个对象,该对象的value值指向另一个对象中的某个属性
setup() {
let person = reactive({
name: "dexter",
age: 18,
job: {
j1: {
salary: 20
}
}
});
const x = toRef(person, "name");
console.log("x=",x);
}
可以看到roRef将person中的name的值也包装成一个对象,如同一个桥梁,联通了操作对象与原对象:
可以利用toRef将定义的对象数据内部的属性取出来,方便在结构中使用
<template>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪资:{{salary}}</h2>
<button @click="name+='~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="salary++">涨薪</button>
</template>
<script>
import { reactive, toRef } from "vue";
export default {
name: "Demo",
setup() {
let person = reactive({
name: "dexter",
age: 18,
job: {
j1: {
salary: 20
}
}
});
return {
name: toRef(person, "name"),
age: toRef(person, "age"),
salary: toRef(person.job.j1, "salary")
}
}
}
</script>
备注:
toRefs和toRef的功能一致,但是可以批量创建多个ref对象,如:const x1 = toRefs(person)
这样可以简化return的语法:
return { ...toRefs(person) }。但是toRefs只能展开第一层数据,深层的数据在结构中依然要通过.来获取。
4、其他Composition API
(1)shallowReactive和shallowRef
-
shallowReactive与
reactive相似,但是shallowReactive只处理对象最外层的响应式(浅响应式) -
shallowRef与
ref相似,但是shallowRef只处理基本数据类型的响应式,不进行对象的响应式处理 -
使用场景
- 如果有一个对象数据,结构比较深,但变化时只是外层属性变化可以使用
shallowReactive - 如果有一个对象数据,后续功能不会修改对象中的属性,而是用新的对象来替换可以使用
shallowRef
- 如果有一个对象数据,结构比较深,但变化时只是外层属性变化可以使用
(2)readonly和shallowReadonly
-
readonly让一个响应式数据变为只读的(深只读)
-
shallowReadonly让一个响应式数据变为只读的(浅只读)
let person = reactive({
name: "dexter",
age: 18,
job: {
j1: {
salary: 20
}
}
});
//person = readonly(person);
person = shallowReadonly(person);
让一个数据变为只读之后不可以修改,如果强制修改控制台会有警告,只读属性的机制是vue内部也不会修改数据,而不是修改了不显示。
(3)toRaw和markRaw
-
toRaw作用是将一个由
reactive生成的响应式对象转换为普通对象。使用场景,用于读取响应式对象对应的普通对象,对这个普通对象的所有操作不会引起页面的更新。
-
markRaw作用是标记一个对象,使其永远不会再成为响应式对象
应用场景,有些值不应被设置为响应式,例如复杂的第三方库等,当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
注意: 使用
markRaw标记成的非响应式数据在vue后台是会检测到修改的,只是不会在页面上显示。
import { markRaw, reactive, toRaw, toRefs } from "vue";
export default {
name: "Demo",
setup() {
let person = reactive({
name: "dexter",
age: 18,
job: {
j1: {
salary: 20
}
}
});
function showRawPerson() {
const p = toRaw(person);
p.age++;
}
function addCar() {
let car = {name: "benzi", price: 40};
person.car = markRaw(car);
}
function changePrice() {
person.car.price++;
}
return { person, ...toRefs(person), showRawPerson, addCar, changePrice }
}
}
(4)customRef
作用是创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制
customRef需要一个函数作为参数,该函数接收两个参数track和trigger,在获取数据前需要调用track通知Vue追踪value的变化,在更新了数据之后要调用trigger通知Vue重新解析模板。
自定义ref实现输入防抖效果
<template>
<input type="text" v-model="keyword">
<h1>{{keyword}}</h1>
</template>
<script>
import { customRef } from 'vue';
export default {
name: 'App',
setup() {
function myRef(value, delay) {
let timer;
return customRef((track, trigger) => {
return {
get() {
console.log(`有人从myRef中读取走了${value}`);
track()
return value;
},
set(newValue) {
console.log(`有人将myRef中的数据修改为${newValue}`);
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue;
trigger()
}, delay)
}
}
})
}
let keyword = myRef("hello", 1000);
return {keyword}
}
}
</script>
(5)provide与inject
实现祖孙组件之间的通信,父组件通过provide选项来提供数据,后代组件通过inject选项来使用这些数据
父组件中写法
setup() {
let car = reactive({name: "benzi", price: "40W"});
provide("car", car)
return {...toRefs(car)}
}
后代组件的写法
setup() {
let car = inject("car");
return {car}
}
(6)响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由
reactive创建的响应式代理 - isReadonly: 检查一个对象是否是由
readonly创建的只读代理 - isProxy: 检查一个对象是否是由
reactive或者readonly方法创建的代理
setup() {
let car = reactive({name: "benzi", price: "40W"});
let sum = ref(0);
let car2 = readonly(car);
console.log(isRef(sum));//true
console.log(isProxy(car));//true
console.log(isReactive(sum));//false
console.log(isReadonly(car2));//true
return {...toRefs(car)}
}
5、Composition API的优势
(1)Options API 存在的问题
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
(2)Composition API 的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
6、新的组件
(1)Fragment
- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
(2)Teleport
Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
<teleport to='body'>
<div v-if="isShow" class="mask">
<div class="dialog">
<h2>这是一个弹窗</h2>
<h4>内容</h4>
<button @click="isShow=false">关闭</button>
</div>
</div>
</teleport>
将需要传送的内容包裹在teleport标签内,通过to属性来指定所要传送的位置,属性值可以是html标签,也可以是css选择器。之后指定的结构就会在目标区域显示了。
(3)Suspense
等待异步组件时渲染一些额外的内容,让应用有更好的用户体验
使用步骤:
-
异步引入组件
import { defineAsyncComponent } from "vue"; const Child = defineAsyncComponent(() => import("./components/Child.vue")); -
使用
Suspense包裹组件,并配置好default与fallback<Suspense> <template v-slot:default> <Child/> </template> <template v-slot:fallback> <h3>加载中,稍等!</h3> </template> </Suspense>当child组件还没有加载成功时会显示加载中,加载成功时就显示child组件。原理是Vue在背后配置了两个插槽,我们需要把适当的内容放在对应的插槽中即可。
7、其他
(1)全局API的转移
Vue 2.x 有许多全局 API 和配置,例如:注册全局组件、注册全局指令等,Vue3.0中对这些API做出了调整:将全局的API,即:Vue.xxx调整到应用实例(app)上
2.x 全局 API(Vue) | 3.x 实例 API (app) |
|---|---|
| Vue.config.xxxx | app.config.xxxx |
| Vue.config.productionTip | 移除 |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
| Vue.prototype | app.config.globalProperties |
(2)其他改变
-
data选项应始终被声明为一个函数。
-
过度类名的更改:
Vue2.x写法
.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }Vue3.x写法
.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; } -
移除keyCode作为 v-on 的修饰符,同时也不再支持
config.keyCodes -
移除
v-on.native修饰符父组件中绑定事件
<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" />子组件中声明自定义事件
<script> export default { emits: ['close'] } </script> -
移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。