学习 Vue3 基础
Vue3 基本概述
介绍
2020 年 9 月 18 日,Vue 发布了 3.0 版本,代号:One Piece(海贼王),周边生态原因,当时大多数开发者还处于观望状态。
现在主流组件库都已经发布了支持 Vue3.0 的版本,例如 Element Plus、Vant、Vue Use,其他生态也在不断地完善中,所以 Vue3 是趋势。
2022 年 2 月 7 日开始,Vue3 也将成为新的默认版本。
优点
Composition API,能够更好的组织、封装、复用代码、RFCs。
性能:打包大小减少 41% 、初次渲染快 55% 、更新渲染快 133%、内存减少 54%,主要原因在于 Proxy,VNode,Tree Shaking support。
Better TS support,源码。
新特性:Fragment、Teleport、Suspense。
趋势:未来肯定会有越来越多的企业使用 Vue3.0 + TS 进行大型项目的开发。
对于个人来说:适应市场需求,学习流行的技术提升竞争力,加薪!
Vite 基本使用
Vite是什么?
- 它是下一代前端开发与构建工具,热更新、打包构建速度更快,但目前周边生态还不如 Webpack 成熟,所以实际开发中还是建议使用 Webpack。
- 但目前就学习 Vue3 语法来说,我们可以使用更轻量的 Vite,例如要构建一个 Vite + Vue 项目,如下。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
Webpack:将所有的模块提前编译、打包进 bundle 中,不管这个模块是否被用到,随着项目越来越大,打包启动的速度自然越来越慢。
Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后相应到客户端。
创建 Vue3 应用
步骤
1.在 main.js
中按需导入 createApp
函数
2.定义App.vue
根组件,导入到 main.js
。
3.使用 createApp
函数基于 App.vue
根组件创建应用实例。
4.挂载至 index.html
的 #app 容器
main.js
// 1. 导入 createApp 函数,不再是曾经的 Vue 了
// 2. 编写一个根组件 App.vue,导入进来
// 3. 基于根组件创建应用实例,类似 Vue2 的 vm,但比 vm 更轻量
// 4. 挂载到 index.html 的 #app 容器
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<div class="container">我是根组件</div>
</template>
<script>
export default {
name: 'App'
}
</script>
选项/组合 API
目标
理解什么是 Options API 写法,什么是 Composition API 写法。
需求
Vue2 实现
- 优点:易于学习和使用,写代码的位置已经约定好。
- 缺点:数据和业务逻辑分散在同一个文件的 N 个地方,随着业务复杂度的上升,可能会出现动图左侧的代码组织方式,不利于管理和维护。
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
// !#Fn1
x: 0,
y: 0,
// ?#Fn2
count: 0,
}
},
mounted() {
// !#Fn1
document.addEventListener('mousemove', this.move)
},
methods: {
// !#Fn1
move(e) {
this.x = e.pageX
this.y = e.pageY
},
// ?#Fn2
add() {
this.count++
},
},
destroyed() {
// !#Fn1
document.removeEventListener('mousemove', this.move)
},
}
</script>
Vue3 实现
优点:可以把同一功能的数据和业务逻辑组织到一起,方便复用和维护。
缺点:需要有良好的代码组织和拆分能力,相对没有 Vue2 容易上手。
注意:为了能较好的过渡到 Vue3.0 版本,目前也是支持 Vue2.x 选项 API 的写法。
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
import { onMounted, onUnmounted, reactive, ref, toRefs } from 'vue'
export default {
name: 'App',
setup() {
// !#Fn1
const mouse = reactive({
x: 0,
y: 0,
})
const move = (e) => {
mouse.x = e.pageX
mouse.y = e.pageY
}
onMounted(() => {
document.addEventListener('mousemove', move)
})
onUnmounted(() => {
document.removeEventListener('mousemove', move)
})
// ?Fn2
const count = ref(0)
const add = () => {
count.value++
}
// 统一返回数据供模板使用
return {
...toRefs(mouse),
count,
add,
}
},
}
</script>
setup 入口函数
介绍
- 是什么:
setup
是 Vue3 中新增的组件配置项,作为组合 API 的入口函数。 - 执行时机:实例创建前调用,甚至早于 Vue2 中的 beforeCreate。
- 注意点:由于执行 setup 的时候实例还没有 created,所以在 setup 中是不能直接使用 data 和 methods 中的数据的,所以 Vue3 干脆把 setup 中的 this 绑定了 undefined,防止乱用!
- 虽然 Vue2 中的 data 和 methods 配置项虽然在 Vue3 中也能使用,但不建议了,建议数据和方法都写在 setup 函数中,并通过 return 进行返回可在模版中直接使用(一般情况下 setup 不能为异步函数)。
使用
<template>
<h1 @click="say()">{{ msg }}</h1>
</template>
<script>
export default {
setup() {
const msg = 'Hello Vue3'
const say = () => {
console.log(msg)
}
return { msg, say }
},
}
</script>
了解:
setup 也可以返回一个渲染函数(setup 中的 return 并非只能返回一个对象)。
<script>
import { h } from 'vue'
export default {
name: 'App',
setup() {
return () => h('h2', 'Hello Vue3')
},
}
</script>
reactive 包装函数
介绍
reactive 是一个函数,用来将普通对象/数组包装成响应式式数据使用(基于 Proxy),无法直接处理基本数据类型!
Vue3 生命周期
介绍
- 组合 API生命周期写法,其实 选项 API 的写法在 Vue3 中也是支持。
- Vue3(组合 API)常用的生命周期钩子有 7 个,可以多次使用同一个钩子,执行顺序和书写顺序相同。
- setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。
toRef
介绍
toRef 函数的作用:转换响应式对象中某个属性为单独响应式数据,并且转换后的值和之前是关联的(ref 函数也可以转换,但值非关联,后面详讲 ref 函数)。
toRefs
介绍
toRefs 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。
ref 函数
介绍
ref 函数,常用于把简单数据类型包裹为响应式数据,注意 JS 中操作值的时候,需要加 .value
属性,模板中正常使用即可。
- 注意:ref 其实也可以包裹复杂数据类型为响应式数据,一般对于数据类型未确定的情况下推荐使用 ref。
- 当你明确知道需要包裹的是一个对象,那么推荐使用 reactive,其他情况使用 ref 即可。
- ref 处理基本数据类型用的是
Object.defineProperty
进行数据劫持,处理复杂数据类型用的是Proxy
(内部借助了 reactive 函数)。
ref 属性
场景
获得单个 DOM 或者组件
<template>
<!-- #3 -->
<div ref="dom">我是box</div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
name: 'App',
setup() {
// #1
const dom = ref(null)
onMounted(() => {
// #4
console.log(dom.value)
})
// #2
return { dom }
},
}
</script>
配合 v-for 循环可以获取一组 DOM 或者组件。
<template>
<ul>
<!-- #4 -->
<li v-for="i in 4" :key="i" :ref="setDom">第 {{ i }} li</li>
</ul>
</template>
<script>
import { onMounted } from 'vue'
export default {
name: 'App',
setup() {
// #1
const domList = []
// #2
const setDom = (el) => {
domList.push(el)
}
onMounted(() => {
// #5
console.log(domList)
})
// #3
return { setDom }
},
}
</script>
computed
作用
computed 函数用来定义计算属性
<template>
<p>firstName: {{ person.firstName }}</p>
<p>lastName: {{ person.lastName }}</p>
<p>fullName: {{ person.fullName }}</p>
</template>
<script>
import { computed, reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '朱',
lastName: '逸之',
})
person.fullName = computed(() => {
return person.firstName + ' ' + person.lastName
})
// 也可以传入对象,目前和上面等价
/* person.fullName = computed({
get() {
return person.firstName + ' ' + person.lastName
},
}) */
return {
person,
}
},
}
</script>
高级
<template>
<p>firstName: {{ person.firstName }}</p>
<p>lastName: {{ person.lastName }}</p>
<input type="text" v-model="person.fullName" />
</template>
<script>
import { computed, reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '朱',
lastName: '逸之',
})
// 也可以传入对象,目前和上面等价
person.fullName = computed({
get() {
return person.firstName + ' ' + person.lastName
},
set(value) {
const newArr = value.split(' ')
person.firstName = newArr[0]
person.lastName = newArr[1]
},
})
return {
person,
}
},
}
</script>
给 computed 传入函数,返回值就是计算属性的值。
给 computed 传入对象,get 获取计算属性的值,set 监听计算属性改变。
watch
监听一个 ref 数据
<template>
<p>{{ age }}</p>
<button @click="age++">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
// 监听 ref 数据 age,会触发后面的回调,不需要 .value
watch(age, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return { age }
},
}
</script>
监听多个 ref 数据
<template>
<p>age: {{ age }} num: {{ num }}</p>
<button @click="handleClick">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
const num = ref(0)
const handleClick = () => {
age.value++
num.value++
}
// 数组里面是 ref 数据
watch([age, num], (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return { age, num, handleClick }
},
}
</script>
立即触发监听
<template>
<p>{{ age }}</p>
<button @click="handleClick">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
const handleClick = () => {
age.value++
}
watch(
age,
(newValue, oldValue) => {
console.log(newValue, oldValue) // 18 undefined
},
{
immediate: true,
}
)
return { age, handleClick }
},
}
</script>
开启深度监听
问题:修改 ref 对象里面的数据并不会触发监听,说明 ref 并不是默认开启 deep 的。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj.hobby.eat</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
watch(obj, (newValue, oldValue) => {
console.log(newValue === oldValue)
})
return { obj }
},
}
</script>
- 解决:当然直接修改整个对象的话肯定是会被监听到的(注意模板中对 obj 的修改,相当于修改的是 obj.value)。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj = { hobby: { eat: '面条' } }">修改 obj</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})
return { obj }
},
}
</script>
- 解决:开启深度监听 ref 数据。
watch(
obj,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
},
{
deep: true,
}
)
监听 reactive 数据
基本操作
注意:监听 reactive 数据时,强制开启了深度监听,配置无效;监听对象的时候 newValue 和 oldValue 是全等的。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">click</button>
</template>
<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
watch(obj, (newValue, oldValue) => {
// 注意1:监听对象的时候,新旧值是相等的
// 注意2:强制开启深度监听,配置无效
console.log(newValue === oldValue) // true
})
return { obj }
},
}
</script>
知识补充
- 想让 ref 内部数据的修改被观测到,除了前面学习的开启深度监听,还可以通过监听 ref.value 来实现同样的效果。
- 因为 ref.value 是一个 reactive,可以通过 isReactive 方法来证明。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
watch(obj.value, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})
return { obj }
},
}
</script>
监听普通数据
- 监听响应式对象中的某一个普通属性值,要通过函数返回的方式进行(如果返回的是对象/响应式对象,修改内部的数据需要开启深度监听)。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>
<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
hobby: {
eat: '西瓜',
},
})
// 不叫普通属性值,是一个 reactive
/* watch(obj.hobby, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}) */
// 叫普通属性值
watch(
() => obj.hobby.eat,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}
)
return { obj }
},
}
</script>
- 监听 ref 数据的另一种写法。
<template>
<p>{{ age }}</p>
<button @click="age++">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
// 监听 ref 数据 age,会触发后面的回调,不需要 .value
/* watch(age, (newValue, oldValue) => {
console.log(newValue, oldValue);
}); */
// 另一种写法,函数返回一个普通值
watch(
() => age.value,
(newValue, oldValue) => {
console.log(newValue, oldValue)
}
)
return { age }
},
}
</script>
watchEffect
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>
<script>
import { reactive, watchEffect } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
hobby: {
eat: '西瓜',
},
})
// 叫普通属性值
/* watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}) */
watchEffect(() => {
// 1. 不指定监视哪一个,这里面用到了谁就监听谁
// 2. 第一次的时候肯定会执行
// 例如对 obj.hobby.eat 的修改,由于这里用到了 obj.hobby.eat,则会执行
// !注意如果这里用的是 obj 则不会被执行
console.log(obj.hobby.eat)
})
return { obj }
},
}
</script>
provide/inject
把 App.vue 中的数据传递给孙组件,Child.vue。
App.vue
<template>
<div class="container">
<h2>App {{ money }}</h2>
<button @click="money = 1000">发钱</button>
<hr />
<Parent />
</div>
</template>
<script>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'
export default {
name: 'App',
components: {
Parent,
},
setup() {
// 提供数据
const money = ref(100)
provide('money', money)
// 提供修改数据的方法
const changeMoney = (m) => (money.value -= m)
provide('changeMoney', changeMoney)
return { money }
},
}
</script>
Parent.vue
<template>
<div>
Parent
<hr />
<Child />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child,
},
}
</script>
Child.vue
<template>
<div>
Child
<p>{{ money }}</p>
<button @click="changeMoney(1)">花 1 块钱</button>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
const money = inject('money')
const changeMoney = inject('changeMoney')
return { money, changeMoney }
},
}
</script>
响应式数据的判断
- isRef: 检查一个值是否为 ref 对象。
- isReactive: 检查一个对象是否是由 reactive 创建的响应式代理。
- isReadonly: 检查一个对象是否是由 readonly 创建的只读代理。
- isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理。
<template>
<div>name: {{ name }}</div>
<div>age: {{ age }}</div>
</template>
<script>
import { reactive, readonly, ref, toRefs, isRef, isReactive, isReadonly, isProxy } from 'vue'
export default {
setup() {
const person = reactive({ name: 'xxx', age: 18 })
const num = ref(0)
const readonlyPerson = readonly(person)
console.log(isRef(num))
console.log(isReactive(person))
console.log(isReadonly(readonlyPerson))
console.log(isProxy(person)) // true
console.log(isProxy(readonlyPerson)) // true
return {
...toRefs(person),
}
},
}
</script>
setup 函数参数
setup 中参数的使用。
父传子
App.vue
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<!-- 1. 父组件通过自定义属性提供数据 -->
<Son :money="money" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const money = ref(100)
return { money }
},
}
</script>
Son.vue
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
</template>
<script>
export default {
name: 'Son',
// 2. 子组件通过 props 进行接收,在模板中就可以使用啦
props: {
money: {
type: Number,
default: 0,
},
},
setup(props) {
// 3. setup 中也可以通过形参 props 来获取传递的数据
console.log(props.money)
},
}
</script>
子传父
App.vue
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<Son :money="money" @change-money="updateMoney" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const money = ref(100)
// #1 父组件准备修改数据的方法并提供给子组件
const updateMoney = (newMoney) => {
money.value -= newMoney
}
return { money, updateMoney }
},
}
</script>
Son.vue
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
<button @click="changeMoney(1)">花 1 元</button>
</template>
<script>
export default {
name: 'Son',
props: {
money: {
type: Number,
default: 0,
},
},
emits: ['change-money'],
setup(props, { emit }) {
// attrs 捡漏、slots 插槽
const changeMoney = (m) => {
// #2 子组件通过 emit 进行触发
emit('change-money', m)
}
return { changeMoney }
},
}
</script>
v-model
基本操作
在 Vue2 中 v-mode 语法糖简写的代码。
<Son :value="msg" @input="msg=$event" />
在 Vue3 中 v-model 语法糖有所调整。
<Son :modelValue="msg" @update:modelValue="msg=$event" />
App.vue
<template>
<h2>count: {{ count }}</h2>
<hr />
<Son :modelValue="count" @update:modelValue="count = $event" />
<!-- <Son v-model="count" /> -->
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const count = ref(10)
return { count }
},
}
</script>
Son.vue
<template>
<h2>子组件 {{ modelValue }}</h2>
<button @click="$emit('update:modelValue', 100)">改变 count</button>
</template>
<script>
export default {
name: 'Son',
props: {
modelValue: {
type: Number,
default: 0,
},
},
}
</script>
传递多个
App.vue
<template>
<h2>count: {{ count }} age: {{ age }}</h2>
<hr />
<Son v-model="count" v-model:age="age" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const count = ref(10)
const age = ref(18)
return { count, age }
},
}
</script>
Son.vue
<template>
<h2>子组件 {{ modelValue }} {{ age }}</h2>
<button @click="$emit('update:modelValue', 100)">改变 count</button>
<button @click="$emit('update:age', 19)">改变 age</button>
</template>
<script>
export default {
name: 'Son',
props: {
modelValue: {
type: Number,
default: 0,
},
age: {
type: Number,
default: 18,
},
},
}
</script>
Fragment
- Vue2 中组件必须有一个跟标签。
- Vue3 中组件可以没有根标签,其内部会将多个标签包含在一个 Fragment 虚拟元素中。
- 好处:减少标签层级和内存占用。
Teleport
作用
传送,能将特定的 HTML 结构(一般是嵌套很深的)移动到指定的位置,解决 HTML 结构嵌套过深造成的样式影响或不好控制的问题。
案例
在 Child 组件点击按钮进行弹框。
<template>
<div class="child">
<dialog v-if="bBar" />
<button @click="handleDialog">显示弹框</button>
</div>
</template>
<script>
import { ref } from 'vue'
import Dialog from './Dialog.vue'
export default {
name: 'Child',
components: {
Dialog,
},
setup() {
const bBar = ref(false)
const handleDialog = () => {
bBar.value = !bBar.value
}
return {
bBar,
handleDialog,
}
},
}
</script>
解决
<template>
<div class="child">
<teleport to="body">
<dialog v-if="bBar" />
</teleport>
<button @click="handleDialog">显示弹框</button>
</div>
</template>
Suspense
异步组件加载期间,可以使用此组件渲染一些额外的内容,增强用户体验。
异步组件
<template>
<div class="app">
App
<Test />
</div>
</template>
<script>
// 静态引入 => 等待所有子组件加载完再统一渲染
// import Test from './Test.vue'
// 动态/异步引入
import { defineAsyncComponent } from 'vue'
const Test = defineAsyncComponent(() => import('./Test.vue'))
export default {
name: 'App',
components: {
Test,
},
}
</script>
优化代码
<template>
<div class="app">
App
<Suspense>
<template v-slot:default>
<Test />
</template>
<template v-slot:fallback>
<div>loading...</div>
</template>
</Suspense>
</div>
</template>
<script>
// 静态引入 => 等待所有子组件加载完再统一渲染
// import Test from './Test.vue'
// 动态/异步引入
import { defineAsyncComponent } from 'vue'
const Test = defineAsyncComponent(() => import('./Test.vue'))
export default {
name: 'App',
components: {
Test,
},
}
</script>
一个细节
setup 也可以返回一个 Promise 实例,但要异步引入此组件并配合 Suspense 使用。
<template>
<div class="test">Test</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Test',
/* setup() {
const count = ref(0)
// 也可以返回 Promise 实例,但要异步引入此组件并配合 Suspense 使用
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ count })
}, 3000)
})
}, */
async setup() {
const count = ref(0)
return await new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ count })
}, 3000)
})
},
}
</script>
script setup
data
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'ifer',
age: 18,
})
const { name, age } = toRefs(state)
</script>
<template>
<h1>name: {{ name }} age: {{ age }}</h1>
</template>
method
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'ifer',
age: 18,
})
const { name, age } = toRefs(state)
// 修改名字
const changeName = () => {
name.value = 'xxx'
}
</script>
<template>
<h1>name: {{ name }} age: {{ age }}</h1>
<button @click="changeName">修改名字</button>
</template>
computed
<script setup>
import { reactive, computed, isRef } from 'vue'
const state = reactive({
firstName: '热',
lastName: '巴',
})
const fullName = computed(() => state.firstName + state.lastName)
console.log(isRef(fullName)) // true
</script>
<template>
<h1>fullName: {{ fullName }}</h1>
</template>
watch
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
<template>
<h1>count: {{ count }}</h1>
<button @click="count++">+1</button>
</template>
props
父组件
<script setup>
import { reactive } from 'vue'
import Hello from './Hello.vue'
const person = reactive({
name: 'ifer',
age: 18,
})
</script>
<template>
<Hello v-bind="person" />
</template>
子组件
<script setup>
// defineProps 无需引用,可以在 script setup 中直接使用
const props = defineProps({
name: String,
age: Number,
})
</script>
<template>
<div>name: {{ props.name }} age: {{ age }}</div>
</template>
emit
父组件
<script setup>
import { reactive } from 'vue'
import Hello from './Hello.vue'
const person = reactive({
name: 'ifer',
age: 18,
})
const updateAge = () => {
person.age++
}
</script>
<template>
<Hello v-bind="person" @updateAge="updateAge" />
</template>
子组件
<script setup>
// defineProps 无需引用,可以在 script setup 中直接使用
const props = defineProps({
name: String,
age: Number,
})
const emit = defineEmits(['updateAge'])
const updateAge = () => {
emit('updateAge')
}
</script>
<template>
<div>name: {{ props.name }} age: {{ age }}</div>
<button @click="emit('updateAge')">update name</button>
<button @click="$emit('updateAge')">update name</button>
<button @click="updateAge">update name</button>
</template>
v-model
父组件
<script setup>
import { reactive } from 'vue'
import Hello from './Hello.vue'
const person = reactive({
name: 'ifer',
age: 18,
})
</script>
<template>
<Hello v-model="person.name" v-model:age="person.age" />
</template>
子组件
<script setup>
// defineProps 无需引用,可以在 script setup 中直接使用
const props = defineProps({
modelValue: String,
age: Number,
})
const emit = defineEmits(['update:modelValue', 'update:age'])
const updateName = () => {
emit('update:modelValue', 'xxx')
}
const updateAge = () => {
emit('update:age', 20)
}
</script>
<template>
<div>name: {{ props.modelValue }} age: {{ age }}</div>
<button @click="updateName">update name</button>
<button @click="updateAge">update age</button>
</template>
defineExpose
- 标准组件写法中,父组件通过 ref 拿到子组件实例,并可以直接访问子组件中的 data 和 method。
- script-setup 模式下,data 和 method 默认只能给当前组件的 template 使用,外界通过 ref 无法访问到。
- 解决:需要手动的通过 defineExpose 进行暴露。
父组件
<script setup>
import { ref, nextTick } from 'vue'
import Hello from './Hello.vue'
const childRef = ref(null)
nextTick(() => {
childRef.value.updatePerson('xxx', 20)
})
</script>
<template>
<Hello ref="childRef" />
</template>
子组件
<script setup>
import { reactive } from 'vue'
const person = reactive({
name: 'ifer',
age: 18,
})
const updatePerson = (name, age) => {
person.name = name
person.age = age
}
// 注意是 defineExpose,不要打成 defineProps 了
defineExpose({
updatePerson,
})
</script>
<template>
<h2>name: {{ person.name }} age: {{ person.age }}</h2>
</template>
slot
父组件
<script setup>
import Hello from './Hello.vue'
</script>
<template>
<Hello>
<!-- 默认插槽 -->
<h2>默认插槽</h2>
<!-- 具名插槽 -->
<template #title>
<h2>具名插槽</h2>
</template>
<!-- 作用域插槽 -->
<template #footer="{ person }">
<h2>通过作用域插槽获取到的数据:{{ person.name }}</h2>
</template>
</Hello>
</template>
子组件
<script setup>
import { reactive, useSlots } from 'vue'
const slots = useSlots()
const person = reactive({
name: 'ifer',
age: 18,
})
// 可以拿到插槽相关的信息
console.log(slots)
</script>
<template>
<slot />
<slot name="title" />
<slot name="footer" :person="person" />
</template>
CSS 变量注入
<script setup>
import { reactive } from 'vue'
const state = reactive({
color: 'pink',
})
</script>
<template>
<h2>Hello Vue3</h2>
</template>
<style scoped>
h2 {
/* 可以使用 v-bind 绑定变量 */
color: v-bind('state.color');
}
</style>
原型绑定与组件使用
main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.year = '再见 2021,你好 2022~~'
app.mount('#app')
App.vue
<script setup>
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
</script>
<template>
<h1>{{ proxy.year }}</h1>
</template>
对 await 支持
<script setup>
const r = await fetch('https://autumnfish.cn/api/joke')
const d = await r.text()
console.log(d)
</script>
<template>
<h1>{{ proxy.year }}</h1>
</template>
定义组件的 name
<template>
<div>Hello</div>
</template>
<script setup></script>
<script>
export default {
name: 'HelloCmp',
}
</script>
mixins
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能,一个混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被“混合”进该组件本身。
// Vue2 写法
Vue.mixin({})
// Vue3 写法
app.mixin({})
follow.js
export const follow = {
data() {
return {
loading: false,
}
},
methods: {
followFn() {
this.loading = true
// 模拟请求
setTimeout(() => {
// 省略请求代码
this.loading = false
}, 2000)
},
},
}
App.vue
<template>
<a href="javascript:;" @click="followFn">{{ loading ? '请求中...' : '关注' }}</a>
<Son />
</template>
<script>
import Son from './Son.vue'
import { follow } from './follow'
export default {
name: 'App',
components: {
Son,
},
mixins: [follow],
}
</script>
Son.vue
<template>
<a href="javascript:;" @click="followFn">{{ loading ? '请求中...' : '关注' }}</a>
</template>
<script>
import { follow } from './follow'
export default {
name: 'Son',
mixins: [follow],
}
</script>