vue-cli 从 4.5.0 版本开始支持 vue3.0
vue-router 从 4.0 版本开始支持 vue3.0
vuex 从 4.0 版本开始支持 vue3.0。其API与3.x基本相同。唯一的突破性变化是插件的安装方式

新增特性

  • Composition(组合)API
  • setup
    • ref 和 reactive
    • computed 和 watch
    • 新的生命周期函数
    • provide 与 inject
  • 新组件
    • Fragment - 文档碎片
    • Teleport - 瞬移组件的位置
    • Suspense - 异步加载组件的 loading 界面
  • 其它 API 更新
    • 全局 API 的修改
    • 将原来的全局 API 转移到应用列表
    • 模板语法变化

小改动

  • destroyed 生命周期选项被重命名为unmounted
  • beforeDestroy 生命周期选项被重命名为beforeUnmount
  • prop default 工厂函数不再有权访问this是上下文
  • 自定义指令API已更改为与组件生命周期一致
  • data 应始终声明为函数
  • 来自 mixin 的 data 选项现在可简单地合并
  • attribute 强制策略已更改
  • 一些过度 class 被重命名
  • 组建 watch 选项和实例方法 $watch 不再支持以点分割的字符串路径。请改用计算属性函数作为参数
  • <template> 没有特殊指令的标记(v-if / else、v-for 或 v-slot)现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容
  • 应用根容器本身不再被视为模板的一部分

组合式API

组合式API基础

setup 组件选项

setup 在创建组件实例之前执行。所以 setup 中没有 this,除 props 外,无法访问组件中声明的任何属性 —— 本地状态计算属性方法

setup 选项是一个接受 propscontext 的函数,该函数返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等)以及组件的模板

Props

setup 函数中的 props 是响应式的。不能使用ES6解构,否则它会消除 props 的响应性。
如果需要解构 prop,可以通过使用 setup 函数中的 toRefs

1
2
3
4
5
6
import { toRefs } from 'vue'

setup(props){
const { title } = toRefs(props)
console.log(title.value)
}

上下文

传递给 setup 函数的第二个参数是 contextcontext 是一个普通的 JavaScript 对象,它暴露三个组件的属性

1
2
3
4
5
6
7
8
9
10
11
12
export default {
setup(props,context) {
// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件(方法)
console.log(context.emit)
}
}

context 是一个普通的 JavaScript 对象,它不是响应式的,所以可以使用ES6解构

1
2
3
4
5
export default {
setup(props,{attrs,slots,emit}){
……
}
}

ref 的响应式变量

可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用

ref 接受参数并返回它包装在具有 value 属性的对象中,然后可以使用该属性访问或更改响应式变量的值

1
2
3
4
5
6
7
8
9
import { ref } from 'vue'

const counter = ref(0)

console.log(counter) //=> { value: 0 }
console.log(counter.value) //=> 0

counter.value++
console.log(counter.value) //=> 1

reftoRef 的区别
1、 ref 本质是拷贝,修改响应式数据不会影响原始数据;toRef 的本质是引用关系,修改响应式数据会影响原始数据
2、 ref 的数据发生改变,界面会自动更新;toRef 当数据发生改变时,界面不会自动更新
3、 toRef 传参与 ref 不同。toRef 接受两个参数,第一个参数是对象,第二个参数是对象的某个属性。如果要遍历对象上的所有属性,可以使用 toRefs

引用传递与值传递

生命周期钩子注册内部 setup

在 3.x,每个生命周期函数都要先导入才可以使用

组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on,即 mounted 看起来像 onmounted

2.x生命周期 3.x生命周期 说明
beforeCreate setup 组件创建前执行
created setup 组件创建后执行
beforeMount onBeforeMount 组件挂载到节点之前执行
mounted onMounted 组件挂载完成后执行
beforeUpdate onBeforeUpdate 组件更新之前执行
updated onUpdated 组件更新完成后执行
beforeDestroy onBeforeUnmount 组件销毁之前执行
destroyed onUnmounted 组件卸载之后执行
errorCaptured onErrorCaptured 当捕获一个来自子孙组件的异常时激活的钩子函数

被包含在 <keep-alive> 中的组件,会多出两个生命周期钩子函数

2.x生命周期 3.x生命周期 说明
activated onActivated 被激活时执行
deactivated onDeactivated 切换组件后,原组件消失前执行

页面第一次进入的时候,钩子函数触发的顺序是 created -> mounted -> activated
页面退出的时候会触发 deactivated,当再次前进或者后退的时候只触发 activated

响应式API

响应式基础API

reactive

返回 对象 的响应式副本。响应式转换是“深层”的。建议只使用响应式代理对象,避免依赖原始对象

  • 返回的代理不等于原始对象
1
2
3
4
5
6
7
8
9
10
import { reactive } from 'vue'

setup(){
const obj = {
name: 0,
psd: 0
}
const userInfo = reactive(obj)
console.log(uerInfo===obj) //-> false
}
  • 修改响应式对象,原始对象也会跟着修改,同时视图更新;修改原始对象,响应式对象也会跟着修改,但是视图不会更新

readonly

isProxy

isReactive

isReadonly

toRaw

markRaw

shallowReactive

shallowReadonly

Refs

ref

接受一个 基本数据类型 的值并返回一个响应式且可变的 ref 对象,响应式对象的值通过 .value 获取。DOM可以直接写变量获取,不需要写 .value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<span>{{count}}</span>
<el-button @click="clickMe">点我</el-button>

import { ref } from 'vue'
setup(){
const count=ref(0)
console.log(count.value) //=> 0
const clickMe=()=>{
count.value++
}
return {
count,
clickMe
}
}

unref

toRef

用来为响应式对象上的属性创建 ref ,然后可以将 ref 传递出去,从而保持对源属性的响应式连接。
为普通对象的属性使用 toRef,也会为该属性创建 ref ,但是修改值得话视图不会更新,普通对象和 ref 的值会同步更改

1
2
3
4
5
6
7
8
const state = reactive({count: 1})
const refState = toRef(state,'count')

refState.value++
console.log(state.count) //=> 2

state.foo++
console.log(fooRef.value) //=> 3

toRefs

解决 toRef 为响应式对象上的多个属性创建 ref 时代码高耦合的问题

1
2
3
4
5
6
7
8
9
10
11
const state = reactive({
foo:1,
bar:2
})
const stateAsRefs = toRefs(state)

state.foo++
console.log(stateAsRefs.foo.value) //=> 2

stateAsRefs.foo.value++
console.log(state.foo) //=> 3

解决响应式对象解构时,丢失响应性的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function userData(){
const state=reactive({
foo:1,
bar:2
})
……
return toRefs(state)
}
export default{
setup(){
const {foo,bar} = userData()
}
return {
foo,
bar
}
}

isRef

customRef

shallowRef

triggerRef

Computed 与 watch

computed

接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象

watchEffect

什么是纯函数?
 1、始终返回相同的值。不管调用该函数多少次,相同的输入都要有相同的输出
 2、不应修改程序的状态或引起副作用
什么是副作用?
 如果有一个函数在输入和输出之外还做了其他的事情,那么这个函数额外做的事情就被成为副作用
VUE 中的副作用?
 响应式数据变更造成的其他连锁反应,以及后续逻辑,这些连锁反应都叫副作用

在监听依赖时立即运行一个函数,并且在依赖更改时重新运行它

1
2
3
4
5
6
7
8
9
const count = ref(0)
const effect = () => console.log(count.value)
watchEffect(effect)

setTimeout(()=>count.value++,200)

// 输出结果
0
1

停止侦听

1、组件卸载时自动停止
2、将 watchEffevt 赋值给一个变量,执行这个变量即可手动停止

1
2
3
4
const stop = watchEffect(()=>{
………
})
stop()

清除副作用

onInvalidate 是 effect 函数传入的参数,是一个函数。用于清除 effect 产生的副作用,只作用于异步函数,并且只有在如下两种情况下才会被调用:
1、当 effect 函数被重新调用时
2、当监听器被注销时(如组件被卸载了)

watch

侦听单个数据源

侦听数据源可以是返回值的 getter函数,也可以直接是 ref

如果直接侦听一个对象,两个参数获取的都是当前值,获取不到修改前的值

1
2
3
4
5
6
7
8
9
10
11
12
// 侦听一个 getter
const state = reactive({count:0})
watch(()=>state.count,(count,preCount)=>{
// count 为当前值,preCount为修改前的值
……
})

// 直接侦听 ref
const count = ref(0)
watch(count,(count,preCount)=>{
……
})

侦听多个源

同侦听单个的方式,数据源为数组类型

1
2
3
4
5
6
7
8
9
10
const count=ref(0)
const bbb=ref(1)
watch([count,bbb],([count,bbb],[preCount,preBbb])=>{
……
})

const count = reactive({aaa:1,bbb:2})
watch(()=>[count.aaa,count.bbb],([aaa,bbb],[preAAA,preBBB])=>{
……
})

与 watchEffect 共享行为

<script setup>

<script setup>是在单文件组件 (SFC) 中使用 组合式API 的编译时语法糖

基本语法

顶层的绑定会被暴露给模板
任何在 <script setup> 声明的顶层的绑定(包括变量、函数声明和 import 引入的内容)都能在模板中直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
// 导入
import { capitalize } from './helpers
// 变量
const msg = 'hello'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<div @click='log'>{{ msg }}</div>
<div>{{ capitalize('hello') }}</div>
</template>

响应式

响应式状态需要明确使用 响应式APIs 来创建。和从 setup() 函数中返回值一样,ref 值在模板中使用的时候会自动解包

1
2
3
4
5
6
7
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click='count++'>{{ count }}</button>
</template>

使用组件

<script setup> 范围里的值也能被直接作为自定义组件的标签名使用

1
2
3
4
5
6
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>

动态组件

正常使用语法

递归组件

一个单文件组件可以通过它的文件名被其自己所引用。这种方法相比于 import 导入的组件优先级更低。如果有命名的 import 导入和组件的推断名冲突了,可以使用 import 别名导入

1
import { FooBar as FooBarChild } from './components'

命名空间组件

可以使用带点的组件标记,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用

1
2
3
4
5
6
7
8
<script setup>
import * as Form from './xxxxx'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>

使用自定义指令

全局注册的自定义指令将以符合预期的方式工作
本地注册的指令必须以 vNameOfDirective 的形式来命名

1
2
3
4
5
6
7
8
9
10
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上的操作
}
}
</script>
<template>
<h1 vMyDirective>his is a Heading</h1>
</template>

definePropsdefineEmits

<script setup> 中必须使用 definePropsdefineEmits API来声明 props 和 emits,它们具备完整的类型推断并且在 <script setup> 中是可以直接使用的

1
2
3
4
5
6
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change','delete'])
</script>
  • definePropsdefineEmits 都是只在 <script setup> 中才能使用的编译器宏。它们不需要导入且会随着 <script setup> 处理过程一同被编译掉
  • defineProps 接收与 props 选项相同的值,defineEmits 接收 emits 选项相同的值
  • definePropsdefineEmits 在选项传入后,会提供恰当的类型推断
  • 传入到 definePropsdefineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量,这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内

defineExpose

使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定

可以使用 defineExpose 编译器宏去暴露 <script setup> 中的属性

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
a,
b
})
</script>

当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref会和在普通实例中一样自动解包)

useSlotsuseAttrs

1
2
3
4
5
6
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

useSlotsuseAttrs 是真实地运行时函数,它会返回与 setupContext.slotssetupContext.attrs 等价的值,同样也能在普通的组合式 API 中使用、

与普通的 <script> 一起使用

<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:

  • 无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项
  • 声明命名导出
  • 运行副作用或者创建只需要执行一次的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 普通 <script>,在模块范围下执行(只执行一次)
runSideEffectOnce()

// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>

<script setup>
// 在 setup() 作用域中执行(对每个实例皆如此)
</script>

顶层 await

<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()

1
2
3
<script setup>
const post = await fetch(`/api/post/1`).then(r=>r.json())
</script>

await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式

async setup() 必须与 Suspense 组合使用

限制:没有 src 导入

<script setup> 不能和 src 属性一起使用

项目实战笔记

项目初始化

入口文件

回顾2.x
2.0 在导入各种依赖之后,通过 new Vue 来执行初始化。相关的插件,有的使用 Vue.use 初始化,有的是作为 new Vue 的入参,如下图所示:
vue2.0初始化

3.x 方式
3.x 是通过 createApp 来执行初始化,其他统一使用 use 来激活初始化
3.x初始化方式