V3
官网:Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)
V3
V3 工程可以通过 vue-cli
脚手架进行创建,但是现在流行用 create-vue
脚手架进行创建 V3 工程
create-vue:这个脚手架是基于 vite
项目构建工具来实现的(vue-cli 是通过 webpack 项目构建工具来实现的)
vite
的特点,一个字 快,服务器启动快,修改代码之后重新响应也很快(vite 也是尤雨溪团队开发的)
要使用 vue-cli 创建 V3 工程,那么 vue-cli 的版本要求最低 4.5.0,可以使用 vue --version
查看版本,如果版本较低可以通过 npm install -g @vue-cli
来进行升级
Vite 脚手架官网:Vite中文网 (vitejs.cn)
创建工程
官方指导:快速上手 | Vue.js (vuejs.org)
在工程目录下使用创建命令:npm create vue@latest
(安装vue-create脚手架并创建V3工程,如果脚手架存在的话不会重新安装)
vite 与 webpack 创建的工程有何不同
入口:index 文件放在了 public 文件夹外面,以 index.html 作为入口,不在以 main.js 作为入口
配置文件:对于 vite 构建工具来说,配置文件是 vite.config.js
他与 webpack 中的 vue.config.js
类似,配置文档 配置 Vite | Vite 官方中文文档 (vitejs.dev)
setup
setup是一个函数,V3中新增的配置项,组件中所用到的data、methods、钩子等都要配置到 setup 中
<template>
<h1>{{ name }}</h1>
<button @click="getName">输出名字</button>
</template>
<script>
export default {
name: 'App',
setup() {
// 定义的变量相当于 data
let name = '123'
// 定义的函数相当于 methods
function getName() { alert(name) }
// 想要使用对应的数据必须进行返回 以 K:V 的方式,如果返回名称一致直接使用变量名也行
return {name, getName}
}
}
</script>
响应式
如果直接在方法中进行更改变量,那么是没有响应式的,也就是说,数据是改了但是不会更新主屏幕
ref 实现
// 导入
import {ref} from "vue";
export default {
setup() {
// 定义的变量使用 ref 进行报错
let name = ref('123')
// 在使用或者更改的时候使用以下方式
name.value = '456'
return {name}
}
}
在模板语法中可以直接使用,不需要 .value
当 data 是对象的时候,可以采让 ref 包裹住对象的方式
import {ref} from "vue";
export default {
setup() {
// 使用 ref 包裹住对象
let user = ref({
name: '张三',
age: 18
hobby: ["足球", "篮球", "羽毛球"]
})
// 当调用方法是进行响应式更改
function getName() {
// 使用以下方式进行更改
user.value.name = '李四'
user.value.age = 19
user.value.hobby[0] = "乒乓球"
}
return {user, getName}
}
}
ref 适合基本数据类型
reactive 实现
reactive 是一个函数,这个实现不能用于基本数据类型,这个函数是专门用于对象类型的
import {reactive} from "vue";
export default {
setup() {
// 使用 reactive 包裹
let user = reactive({
name: '张三',
age: 18,
hobby: ["足球", "篮球", "羽毛球"]
})
function getName() {
// 在修改的时候不需要使用 value 就可以直接修改
user.name = '李四'
user.age = 19
user.hobby[0] = "乒乓球"
}
return {user, getName}
}
}
使用以上的方式也可以对盒子套盒子的对象进行改变,并且对于 中途添加一个属性、中途删除一个属性(使用 delete 对象.属性
)、通过数组下标直接访问 也是可以响应式的
props
父组件
<template>
<User :age="user.age" :name="user.name"/>
</template>
<script>
import User from "@/components/User.vue";
export default {
name: 'App',
// 导入组件
components: {User},
setup() {
let user = {
name: '张三',
age: 18,
}
return {user}
}
}
</script>
子组件
<template>
<h1>{{ user.name }} - {{ user.age }}</h1>
</template>
<script>
export default {
name: "User",
// 需要声明 props
props: ["name", "age"],
setup(props) {
return {user: props}
}
}
</script>
V3在调用 setup 函数之前会传递参数,第一个是 props。它被包装成了一个代理对象,具有响应式的 proxy 对象
defineProps
这种方式是Vue3使用语法糖之后的写法,具体为以下
// 导入 — 这很重要
import {defineProps} from 'vue';
// 使用 defineProps 方法并使用链表的方式传入要接收的数据
const props = defineProps(['content']);
生命周期
跟V2的声明周期作比较
- 生命周期最后两个钩子由原来的
beforeDestory
、destoryed
变为beforeUnmount
、unmounted
setup
函数执行在beforeCreate
之前
在V3中生命周期钩子函数由两种写法:选项式、组合式
选项式API:在 setup 外面写生命周期钩子(V2的写法)
组合式API:在 setup 中写生命周期钩子,并且缺少 beforeCreate
、beforeed
因为 setup 代替了(如果想使用这两个钩子的话,只能采用选项式的写法)。
// 从这里导入对应的钩子
import {onBeforeMount} from "vue";
export default {
// ...
setup() {
onBeforeMount(() => {
// ...
});
// ...
}
}
自定义事件
父组件
<template>
<!-- 创建自定义事件 -->
<User @event="log"/>
</template>
<script>
import User from "@/components/User.vue";
export default {
name: 'App',
components: {User},
setup() {
// 事件触发时输出内容
function log(name, sex) {
alert(`姓名:${name},性别:${sex}`)
}
return {log}
}
}
</script>
子组件
<template>
<button @click="trigger">点击触发自定义事件</button>
</template>
<script>
export default {
name: "User",
setup(props, context) {
function trigger() {
// 触发自定义事件 并传参
context.emit("event", "张三", "男")
}
return {trigger}
}
}
</script>
setup 的第二个参数是组件的上下文对象,然后使用 emit 触发事件,第一个是事件的名称,往后所有都是传递的参数
全局事件总线
V3的全局事件总线需要借助第三方库 mitt
安装
安装:npm i mitt
然后封装一个 src/util/event-bus.js 的文件
import mitt from "mitt";
export default mitt();
使用
父组件
<template>
<User/>
</template>
<script>
import User from "@/components/User.vue";
import {onMounted} from "vue";
// 导入全局事件总线对象
import emitter from './util/event-bus'
export default {
name: 'App',
components: {User},
setup() {
// 生命周期钩子 mounted
onMounted(() => {
// 给全局事件总线绑定事件 | 绑定事件 - 触发的函数
emitter.on("event", (user) => {
console.log(user.name, user.sex)
})
})
}
}
</script>
子组件
<template>
<button @click="trigger">点击触发自定义事件</button>
</template>
<script>
import emitter from '../util/event-bus'
export default {
name: "User",
setup(props, context) {
function trigger() {
// 触发自定义事件 并传参
emitter.emit("event", {
name: "张三",
sex: "男"
})
}
return {trigger}
}
}
</script>
在以上的代码中通过全局事件总线实现的事件的绑定与触发。但是,与自定义事件不同的是,全局事件总线传递的参数只有一个,并且也不需要在组件上进行 @事件名
了
在使用是需要导入总线对象
import emitter from './util/event-bus'
绑定事件:emitter.on("事件名称", 回调函数)
触发事件:emitter.emit("事件名称", 参数)
解绑事件(全部):emitter.all.clear()
解绑事件(指定):emitter.off("事件名")
计算属性
在V3中计算属性是组合式写法,也就是得写在 setup 中
import {computed} from "vue";
export default {
name: 'App',
setup() {
// 简写
let com = computed(() => {
return xxx;
})
// 完整写法
let com = computed({
set(value) { ... },
get() { return xxx }
})
// 返回
return {com}
}
}
简写:如果只需要使用计算属性的 get 方法的情况下,就可以使用简写
完整:这种写法是包含 get/set 的方法的适合在需要直接改变计算属性的情况下使用
侦听属性
被监视的属性必须具有响应式才能够监听
侦听 ref
<template>
<input type="text" v-model="name"/>
</template>
<script>
import User from "@/components/User.vue";
import {ref, watch} from "vue";
export default {
name: 'App',
components: {User},
setup() {
let name = ref("张三")
// 参数:被侦听的属性 回调函数 配置信息
watch(name, (newValue, oldValue) => {
console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
}, {
// 立即执行
immediate: true,
// 深度监听
deep: true
})
return {name}
}
}
</script>
在上面的 watch 写法中,如果由多个要被侦听的属性就需要写多个 watch 但是,如果某些属性执行的逻辑是相同的,可以采用数组的形式进行简写
watch([name, age], (newValue, oldValue) => {
console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
}, {
// 立即执行
immediate: true,
// 深度监听
deep: true
})
因为采用的是数组,那么新数据和旧数据打印的也是数组,需要获取某个属性的新老数据时,采用数组的形式获取
当 ref 包裹的是对象的时候,监视属性的位置如果填写:xxx.value
,就相当于下面的 reactive 深度监视是开启的,但是如果直接填写 xxx
那就可以控制深度监视(默认关闭),想要监视单一属性就需要使用函数进行返回
// 默认不开启深度监视
watch(user, (newValue, oldValue) => {
console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
}, {deep: true})
watch(user.value, (newValue, oldValue) => {
console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
})
watch(() => user.value.name, (newValue, oldValue) => {
console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
})
侦听 reactive
let user = reactive({
name: "张三",
age: 18
})
watch(user, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
在侦听 reactive 函数获取的代理对象时,只能获取最新数据(newValue)老数据(oldValue)无法获取。并且对于代理对象来说,默认开启深度监视,而且这种深度监视是不可关闭的
如果想要进行单一的属性,不能直接 对象.属性
这种事错误的,应该采用一个函数把需要侦听的属性进行返回
watch(() => user.name, (newValue, oldValue) => {
console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
})
但是对于 对象.属性
是错误的也不是很对,在有一种场景下可以使用这种方式,那就是这个属性也是一个对象
let user = reactive({
name: "张三",
age: 18,
xxx: {
a: 1,
b: 2
}
})
在以上的情况下是可以直接 user.xxx
进行监听的,但是这种方式的老数据也是不能拿到的
在 reactive 中如果逻辑相同的话也是可以通过数组的形式来进行减少代码的,在
[]
中也是需要写函数的形式
watchEffect
这是 V3 新增的组合式API,作用就是返回监控数据变化。用法在里面跟上回调函数
setup() {
let user = reactive({
name: "张三",
age: 18,
})
watchEffect(() => {
const n = user.name
})
return {user}
}
回调函数的执行时机,按照上面的写法,每当 user.name
发生变化时,就会执行回调函数,如果有多个也是同理。但是 age 的变化不会让回调函数执行
自定义钩子函数(hook)
这个自定义钩子与V2中的 mixin 有点相同都是为了让代码得到复用
使用:在 src 目录下创建一个 hooks 的目录,然后在其中创建 js 文件(文件名一般是功能名)
// 这是一个简单的求和函数
export default function (a, b) {
return a + b;
}
在使用的位置进行导入即可正常使用
import sum from "@/hooks/sum.js";
浅层次的响应式(shallow)
shallowReactive:对象第一层支持响应式,第二层就不再支持了
shallowRef:只给基本数据类型添加响应式,如果是对象,则不会给它添加响应式
深只读与浅只读
组合式API:readonly 和 shallowReadonly
场景:如果从其他组件传递过来的数据不希望修改,那么最好加上一个只读
深只读:具有响应式的对象中所有的属性,包括子对象中的子对象中的属性,它的所有值都是只读的不可修改的
使用方法只需要对要被只读的数据用 readonly 进行包裹
浅只读:具有响应式的对象第一层的值是只读的
响应式数据的判断
isRef:检查是否为ref对象
isReactive:检查是否是由 reactive()
或者 shallowReadonly()
创建的代理
isProxy:检查一个值是否是由 reactive()
、readonly()
、shallowReactive()
或 shallowReadonly()
创建的代理
isReadonly :检查传入的值是否为只读对象
使用:从 vue 中导入相对应的内容然后直接使用即可
toRef 与 toRefs
在使用插值语法的的时候输入某些值时前面需要写很长的属性(xxx.xxx.xxx.age
),如果要省略这些 xxx 那么可以在 return 返回的时候使用键值对的方式返回
let user = reactive({
name: "IKun",
age: 3,
hobby: {
h1: "唱",
h2: "跳",
h3: "rap",
h4: "打篮球"
}
})
return {
h1: user.hobby.h1,
h2: user.hobby.h2,
h3: user.hobby.h3,
}
toRef
按照以上的方式就可以直接使用 h1 进行访问内容了,但是这种方法没有响应式,也就是说,更改 h1 的值 user 中的值不会更改。也可以直接使用 ref 再次进行包裹(h1: ref(user.hobby.h1)
)这种方式倒是有响应式,但是相当于新建的一个响应式对象,更改的也是这个新的,不会对老的 user 进行更改
在这种情况下就可以使用 toRef
进行包裹住,这样返回的数据不仅仅是老数据的而且也具有响应式
return {
h1: toRef(user.hobby, "h1"),
h2: toRef(user.hobby, "h2"),
h3: toRef(user.hobby, "h3"),
h4: toRef(user.hobby, "h4"),
}
toRef 的使用的两个参数,第一个参数是对象,第二个参数是对象中的属性名
toRefs
上面的方式有点过于繁琐,所以又有一个叫 toRefs 的内容
return {
...toRefs(user.hobby)
}
在 toRefs 中填入 user 的话,那么会给 user 的所有内容做代理。使用时,需要从当前层次进行使用,如上所示,在上面的写法中只给 user 内的 hobby 做了代理,使用时只能使用 hobby 内的数据,使用时直接使用属性名即可,如果单纯给 user 做代理,那么想要访问 h1 就得写成 hobby.h1
的形式
由于 toRefs 返回的是对象,所以采用 es6 语法(...xxx)进行添加返回
以上方式对 ref 和 reactive 都有相同的效果
(转换 | 标记)为原始
(转换 | 标记)为原始意思就是把响应式数据(转换 | 标记)成普通对象或者数据
转换(toRaw):将响应式对象转换为普通对象。只适用于reactive 生成的响应式对象
标记(markRaw):标记某个对象,让这个对象永远都不具备响应式
使用:直接对对象进行包裹并接收即可
Fragment 组件
Fragment翻译:碎片、片段
在V2中每个组件必须有一个根标签,在组件嵌套组件的时候性能问题,因为有些时候没必要去嵌套这些根标签。然后再V3中没有根标签或者说有多个根标签,实际上再所有标签嵌套完成后会再最外层添加一个 Fragment 用这个当根标签
Teleport 组件
Teleport翻译:远距离传送、瞬间移动。用于设置组件的显示位置
举个例子,再组件套组件中如果想要对最下层的某一个盒子做定位,那么定位是要有参照物的默认时body,但是如果某一层的组件添加上的某个属性作为了参照物,那么就会导致效果混乱,而 Teleport 组件就是把搬来应该再最下层的盒子移动到指定的位置,比如body的子盒子
<template>
<Teleport to="[标签名|ID选择器]">
<User />
</Teleport>
</template>
provide & inject 传递数据
在这种数据传递方式中,适合在祖宗组件对孙子组件传递数据的场景
// 先声明
provide("名称", 数据);
// 取值
let aaa = inject("名称");
自定义 ref
import {customRef} from "vue";
export default {
name: 'App',
setup() {
// 自定义 ref
function customizeRef(value) {
// 返回需要是一个响应式的 ref
return customRef((track, trigger) => {
// 自定义 ref 的 get 和 set 并返回
return {
get() {
// 追踪依赖
track()
return value;
},
set(newValue) {
value = newValue
// 触发依赖
trigger()
}
}
})
}
let hello = customizeRef("你好,世界");
return {hello}
}
}
通过以上的方式进行自定义一个 ref 这样做的目的是在输入和返回数据的时候进行夹带点私货
setup 语法糖
只需要把标签加入setup <script setup>
这样
export default {
name: "User",
components: {xxx},
setup(props, context) {
return {xxx}
}
}
那么像以上这样的代码就不需要写了,组件导入之后不需要注册直接用
props
对于加了语法糖之后就没有了获取 props 内容的地方,vue提供了一个函数 defineProps()
只需要在这里面声明发送过来的属性名即可使用
// 数组形式
defineProps(["name", "age"]);