vue3基础知识梗概
写在前面:阅读本文需要掌握 vue2,本文不会特别回顾 vue2 中的知识
Composition API
1. setup
- 组件中所用到的数据,方法等都需要配置在 setup 中.
- setup 函数的两种返回值:
- 若返回一个对象,则对象中的属性,方法在模板中均可以直接使用
- 若返回一个渲染函数(h),则可以自定义渲染内容
- 注意点
- 尽量不要与 vue2 混用
- 在非异步组件中 setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 promise,在模板中就无法使用了
2. ref
- 作用: 定义一个响应式数据
- 语法:
const xxx = ref(value)
- 创建一个包含响应式数据的引用对象
- 操作数据需要使用
xxx.value
- 模板中插值表达式不需要使用.value
- 备注
- 接受的数据可以是基本类型也可以是对象.
- 基本类型数据的响应式仍然是使用的
Object.defineProperty()
的 set get 完成的 - 对象类型属性的响应式使用了 reactive 函数,本质是 Proxy
3. reactive 函数
- 作用: 专用于定义一个对象类型的响应式数据
- 语法:
const 代理对象 = reactive(源对象)
接受一个对象(数组),返回一个代理对象(Proxy) - reactive 的响应式数据是深层次的.不用再像 vue2 一样要使用
Vue.set()
来进行更新对象 - 是基于 ES6 的 Proxy 实现的,通过代理对象操作源对象内部数据进行操作
4. 响应式原理
-
通过 Proxy 拦截对象中任意属性的变化.Proxy 可以理解成在目标对象之前架设防火墙,外界对该对象的访问都必须经过防火墙.因此这就提供了过滤功能,这里用来表示由它来代理某些操作,称为代理器.
-
通过 Reflect 对对象的属性进行操作.Reflect 将 Object 对象的一些明显属于语言内部的方法(如
Object.defineProperty
)放到 Reflect 对象上.某种意义上来讲,Reflect 就是另一种写法而已.但未来的新方法将之不是在 Reflect 对象上.也就是说,从 Reflect 对象上可以拿到语言内部的方法.
let person = {
_name: "Json",
age: 18,
};
let handler = {
// get捕获读取操作
get(origin, prop) {
if (Reflect.has(origin, prop)) {
return Reflect.get(origin, prop);
} else {
throw new ReferenceError("不存在该属性");
}
},
// set捕获赋值操作
set(origin, prop, value) {
if (prop[0] === "_") {
throw new Error("只读属性");
}
if (prop === "age") {
if (!Number.isInteger(value)) throw new TypeError("请输入整数");
if (value > 200) throw new RangeError("数字太大");
}
return Reflect.set(origin, prop, value);
},
// deleteProperty捕获删除操作
deleteProperty(origin, prop) {
console.log(`删除${prop}属性`);
return Reflect.deleteProperty(origin, prop);
},
};
let proxy = new Proxy(person, handler);
console.log(proxy._name); // Json
console.log(delete proxy.age); // true
上面的代码是一个小 demo,示例了下 Proxy 以及 Reflect 的使用.如果想要学习这两个,请查看阮一峰的 ES6 教程
5. setup 注意点
- setup 在
beforeCreate
之前执行,没有this
- setup 的参数
props
:值为对象,包含组件外部传递过来且组件内部声明接受了的属性context
:上下文对象- attrs:值为对象,包含组件外部传递过来但没有在 props 配置中声明的属性,相当于 vue2 中的
this.$attrs
- slots:收到的插槽内容,相当于 vue2 中的
this.$slots
- emit:分发自定义事件的函数,相当于 vue2 中的
this.$emit
- attrs:值为对象,包含组件外部传递过来但没有在 props 配置中声明的属性,相当于 vue2 中的
6. 计算属性以及监视
computed
<script lang="ts" setup>
import { computed, reactive, WritableComputedRef } from "vue";
interface p { // 定义Person要使用的接口
firstName: string; // 一般字符串类型
lastName: string; // 一般字符串类型
fullName: WritableComputedRef<string>; // computed计算属性的数据类型
}
let person: p = reactive({ // 使用reactive实现深度监听
firstName: "",
lastName: "",
fullName: null,
});
// 对象.属性 = computed()的方式实现计算属性 参数可以为对象或者函数,内容与vue2一致
person.fullName = computed({
set(value: string) { // 捕获更改
const valueArr: Array<string> = value.split(" ");
person.firstName = valueArr[0];
person.lastName = valueArr[1];
},
get() {
if (person.firstName !== "" && person.lastName !== "") {
return person.firstName + " " + person.lastName;
}
},
});
</script>
上面的代码可以实现一个姓+名拼接成全名的表单,并且还能通过更改全名的方式直接更改姓或名
watch
- 监听 ref 响应式数据
const a = ref(0);
watch(a, (newVal, oldVal) => {
console.log("a被改变了");
});
- 监听 ref 多个数据
const b = ref(100);
const c = ref(0);
watch([b, c], (newVal, oldVal) => {
console.log("b或c变化了", newVal, oldVal);
});
- 监听 reactive 对象
interface per {
name: string;
age: number;
}
const person: per = reactive({
name: "Tom",
age: 12,
});
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: false }
); // deep:false不生效
这里需要注意的是,监听 reactive 定义的响应式对象,是无法取消深度监听的,并且你还会发现无法正确获取到老值oldVal
想要获取到oldVal
应该使用computed
去缓存数据
const person = reactive({
name: "Json",
age: 19,
});
let P = computed(() => {
return JSON.parse(JSON.stringify(person));
});
watch(P, (newVal, oldVal) => {
console.log(newVal, oldVal); // 能够正确获取到oldVal
});
- 监听对象中的单个数据
interface per {
name: string;
age: number;
}
const person: per = reactive({
name: "Tom",
age: 12,
});
watch(
() => person.age,
(newVal, oldVal) => {
console.log("年龄变化了");
}
);
第一个参数变成了一个具有返回值的函数,返回值为需要监听的属性,使用箭头函数的写法就显得非常简洁了
- 监听对象中的一些数据
interface per {
name: string;
age: number;
}
const person: per = reactive({
name: "Tom",
age: 12,
});
watch([() => person.name, () => person.age], (newVal, oldVal) => {
console.log();
});
第一个参数成了数组,数组内部有多个函数返回值,返回值为要监听的属性
- 监听 reactive 对象中的普通对象
interface per {
name: string;
age: number;
job: wo;
}
interface wo {
work: string;
}
const person: per = reactive({
name: "Sam",
age: 24,
job: {
work: "front-end",
},
});
watch(
() => person.job,
(newVal, oldVal) => {
console.log("job发生变化");
},
{ deep: true }
); // deep:true不能去掉
需要注意的是deep:true
不能去掉,因为在这里监听的是一个普通的对象,所以 deep 配置有效
watchEffect 函数
-
watch:既要指明监视的属性,也要指明回调.
-
watchEffect:不用直接指明监视的属性,在回调中使用到哪个属性就监视哪个属性,并且在初始化的时候立即执行一次(相当于 watch 里的 immediate:true).
-
watchEffect 与 computed 函数的区别
- computed 更注重返回的值,computed 函数必须要有返回值.
- watchEffect 更注重函数的执行,可以没有返回值.
watchEffect(() => {
const x = sum.value; // 监视ref定义的sum
const y = person.age; // 监视reactive定义的person对象的age属性\
//...逻辑
});
7. 生命周期
在 vue3 中可以继续使用 vue2 声明生命周期的方式.但是有两个被更名
beforeDestroy()
=>beforeUnmount()
destroyed()
=>unmounted()
vue3 中也提供了组合式 API 中声明的方式(就是在setup()
中写的),与 vue2 中的钩子对应关系如下
beforeCreate()
=setup()
因此就不需要声明了created()
=setup()
因此就不需要声明了beforeMount()
=onBeforeMount()
mounted()
=onMounted()
beforeUpdate()
=onBeforeUpdate()
updated()
=onUpdated()
beforeUnmount()
=onBeforeUnmount()
unmounted()
=onUnmounted()
8. 自定义 hook 函数
hook 本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装.
类似于 vue2 中的 mixin.
自定义 hook 带来的优势:复用代码,让 setup 内部结构更简单更易懂.
<script setup lang="ts">
import usePoint from "../hooks/usePoint"; // 引用
const point = usePoint(); // 使用自定义hooks
</script>
9. toRef && toRefs
引子:
setup(){
const person = reactive({
name: "Tom",
age: 23,
job: {
name: "front-end",
salary: 30,
},
});
return{
name:person.name,
age:person.age
}
}
上面的代码在页面中可以使用插值表达式来表达出name
以及age
,但是这个是不能更改的,是不具有响应式的.因为person.name
的本质就是一个字符串,怎么进行响应式?
这就引出 toRef 了
toRef
作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性.
语法:const name = toRef(obj,"propName")
应用:要将响应式对象中的某个属性单独提供给外部使用时
setup(){
//...
return {
name:toRef(person, "name"),
age:toRef(person, "age")
}
}
这样做的好处是,在模板中可以直接写{{name}}
,{{age}}
来获取到值,并且还能对其做出改变.
toRefs
与toRef
功能一致,但可以批量创建多个 ref 对象
语法:toRefs(obj)
用法:return 的时候直接...toRefs(obj)
即可
其他 Composition API
shallowReactive && shallowRef
shallowReactive
函数返回的数据只处理对象最外层的响应式(浅响应).
shallowRef
函数返回的数据只处理基本数据类型的响应式.
什么时候使用?
- 如果有一个对象数据,结构很深,但变化时只是外层数据发生变动,此时应该使用
shallowReactive
. - 如果有一个对象数据,后续不会修改里面的属性,而是直接替换整个对象,此时应该使用
shallowRef
.
readonly && shallowReadonly
readonly
接收一个数据,函数返回的数据是只读的,并且是深层次的只读
shallowReadonly
接收一个数据,函数返回的数据是浅层只读的,只有最外层的数据是只读的
用法: const xxx = readonly(person)
后续中转变量xxx
的属性就不能被更改了,当然原对象person
是能更改的,只是xxx
不能被更改.
toRaw && markRaw
toRaw
将一个由reactive
生成的响应式对象转为普通对象
markRaw
标记一个对象使其不再做响应式.比如临时往响应式对象中添加新数据,由于新增的数据也是响应式的,可以使用markRaw
使其变成非响应式数据.
- 使用场景:
- 有些值不应被设置为响应式,例如复杂的第三方库.
- 当渲染具有不可变数据源的大列表时,跳过响应式以提升性能.
自定义 ref -- customRef
定义:创建一个自定义的 ref,并对其依赖项跟踪(track)和更新触发(trigger)进行显示控制
用法:
setup(){
function myRef(value){
// 这个自定义的函数应该返回一个Vue的customRef,因为不可能让开发者自己写一套完整的响应式代码
return customRef((track,trigger)=>{ // track:跟踪器 trigger:触发器
// 必须返回一个对象,对象内必须含有getter,setter
return{
get(){
track() // 让Vue跟踪value
return value
},
set(newVal){
value = newVal // 让旧值等于新值
trigger() // 触发更新界面
}
}
})
}
const x = myRef(100)
return{
x
}
}
provide && inject
作用:实现祖组件与后代组件通信
使用:
- 在祖组件中
setup(){
const something = "string"
provide("prop",something)
}
- 在后代组件中
setup(){
const something = inject("prop")
return{
something
}
}
响应式数据的判断
isRef
:检查一个值是否为一个 ref 对象.isReactive
:检查一个值是否是由reactive
创建的响应式代理.isReadonly
:检查一个对象是否是由readonly
创建的只读代理.isProxy
:检查一个对象是否是由reactive
或者readonly
方法创建的代理.
Composition API 的优势
新的组件
Fragment
正是因为这个组件,让 template 模板中不在需要一个根元素包裹所有的标签.Fragment 会自动将所有的标签自动包裹.
Teleport
使用 Teleport 组件能够将 HTML 结构移动到指定地方
<!-- to后面跟要移动的位置 -->
<teleport to="body">
<div>
...
</div>
</teleport>
作用:让 css 样式更好设置
异步组件 && Suspense
Suspense 在等待组件渲染的时候给予用户更好的体验
在使用的时候需要引入异步组件
import { defineAsyncComponent } from "vue";
const Child = defineAsyncComponent(() => import("./components/Child.vue"));
上面的代码中defineAsyncComponent
接受一个参数,该参数是一个函数,函数的返回值为一个import
函数,import 函数中包含需要异步引入的组件
在异步组件中,setup
可以为一个async
函数,返回一个 promise.
Suspense 的用法:
使用Suspense
包裹两个<template>
,两个 template 都需要配置v-slot
,并配置好插槽名为default
与fallback
<template>
<h2>这是子组件</h2>
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</template>
上面的代码中的v-slot:default
与v-slot:fallback
是固定写法,不能改名
default
插槽内写加载完成后需要渲染的组件,fallback
里写在加载过程中要给用户的提示信息.
其他更改
全局 API 的转移
-
Vue 2.x 有许多全局 API 和配置。
-
例如:注册全局组件、注册全局指令等。
//注册全局组件 Vue.component('MyButton', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>' }) //注册全局指令 Vue.directive('focus', { inserted: el => el.focus() }
-
-
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
-
其他改变
-
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” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
v-model的参数
若子组件有属性值是动态的,而这个值又是父组件给予的,那么采用这种方式将能够很好的处理这种情况.
- 父组件
<template>
<input type="text" v-model="value">
<Child v-model:title="value"></Child>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const value = ref("")
</script>
- 子组件
<template>
<h2>{{title}}</h2>
</template>
<script setup>
const props = defineProps(["title"]) // 使用defineProps能获取父组件传过来的值
</script>
上面的代码能够实现子组件的标题是随着父组件中的value值改变而改变
setup 语法糖
格式:
<script setup>
// 代码
</script>
特性:
- 不需要再返回声明的变量和函数
- 不需要再 export default{}
- 不需要再注册组件
当然也是可以获取 context,props,emits 的,这确实是学习成本,但很简单
使用 useAttrs
useSlots
获取原本 setup(context)里的内容
<script setup>
import { useAttrs, useSlots } from "vue";
const attrs = useAttrs();
const slots = useSlots();
</script>
使用 defineExpose
暴露子组件内部的数据,父组件仍使用ref
属性接受
子组件
<script setup>
import { defineExpose } from "vue";
const a = "暴露的内容";
defineExpose({
a,
});
</script>
父组件
<template>
<Child ref="chi" />
</template>
<script setup>
import {onMounted, ref} form "vue";
import Child from "./Child.vue";
const chi = ref(null)
onMounted(()=>{
console.log(chi.value.a)
})
</script>
使用 defineProps
接受父组件传过来的值,仍可以指定接受的类型
父组件
<template>
<Child :msg="message">
</template>
<script setup>
import Child from "./Child.vue";
const message = "一般的父向子传值"
</script>
子组件
<template>
<h2>{{ msg }}</h2>
</template>
<script setup>
defineProps(["msg"]);
</script>
使用 defineEmits
子组件像父组件传值
子组件
<template>
<button @click="toParent">触发向父组件传值事件</button>
</template>
<script setup>
const emit = defineEmits(["child"]); // 相当于告诉了父组件有这么一个东西
function toParent() {
emit("child", "子向父传值"); // emit("名字","内容")
}
</script>
父组件
<template>
<Child @child="readChildMsg" />
</template>
<script setup>
import Child from "./Child.vue";
function readChildMsg(msg) {
console.log(msg);
}
</script>
确实很奇怪,相比较 vue2 的this.$emit
,defineEmits
还多了一步