# vue3保姆级教程
分享 15 个 Vue3 全家桶开发的避坑经验 (opens new window)
如何写一个属于自己的Vue3组件库 (opens new window)
如何为 Vue3 组件标注 TS 类型 (opens new window)
# Vue3简介
面临的问题:随着功能的增长,复杂组件的代码变得难以维护,Vue3
就随之而来,TypeScript
使用的越来越多,Vue3
就是 TS
写的所以能够更好的支持 TypeScript
在这里介绍就这么简单
vue2
的绝大多数的特性 在 Vue3
都能使用,毕竟 Vue
是渐进式的
响应式原理进行使用 Proxy
实现,v-model
可以传参了等等新特性
# 基础工作
使用Vue3
的话,那么必须通过使用构建工具创建一个 Vue3
项目
# 安装 vue-cli
# npm
npm install -g @vue/cli
# yarn
yarn global add @vue/cli
2
3
4
# 创建一个项目
使用 create
命令行创建 或者 用 ui
可视化创建
大家用 Vue
都用了这么久,我就不一一说怎么去创建了
# create
vue create 项目名
# 可视化
vue ui
2
3
4
# vite
当然也可以选择 vite
,vite
创建的速度比 上面的方法快了一些
npm init vite-app 项目名
cd 项目名
npm install
npm run dev
2
3
4
都创建好了吧,创建好了我们就来讲一讲vite (opens new window)😝
第一我们要先了解 vite 是什么,vite是尤雨溪团队开发的新一代的前端构建工具,意图取代 webpack ,首先我们先来看一看vite有什么优点
- 无需打包,快速的冷服务器启动
- 即时热模块更换(HMR,热更新)
- 真正的按需编译 webpack是一开始是入口文件,然后分析路由,然后模块,最后进行打包,然后告诉你,服务器准备好了(默认8080)
然而vite是什么,它一开始是先告诉你服务器准备完成,然后等你发送HTTP请求,然后是入口文件,
Dynamic import
(动态导入)code split point
(代码分割) 如何使用vite呢,大家可以去看官网 (opens new window)
//要构建一个 Vite + Vue 项目,运行,使用 NPM:
npm init @vitejs/app 项目名
//使用 Yarn:
yarn create @vitejs/app 项目名
你会觉得非常快速的创建了项目,然而它并没有给你下载依赖,你还有进入文件然后
npm install (or yarn)
2
3
4
5
6
然后它的打开方式不是 serve
变成了dev
Edit components/HelloWorld.vue to test hot module replacement.
编辑components/HelloWorld.vue以测试热模块更换。(也就是热更新更快)
然而我们只是简单了解下,我们现在的重点是vue3,如果以后vite成为主流,我们也可以在回头看看。😜
现在还是以主流的方式创建并进行讲解
# 基本了解
当我们创建完成vue3项目后,点击它的main.js,你会发现写法发生了改变
引入的不是vue构造函数,而是createApp
工厂函数然而,创建实例对象其实就相当于vue2中的vm
,mount('#app')
就相当于$mount('#app')
,并且vue2的写法在vue3不能兼容
现在我们进入App组件,你会发现什么不一样的地方,他没有了根标签,在vue2的时候,我们都是在div根标签里面写东西,所以在vue3里面可以没有根标签
# Vue3入门
# Fragment
在
template
中不再需要一个根元素包裹
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</template>
2
3
4
实际上内部会将多个标签包含在一个Fragment
虚拟元素中
好处: 减少标签层级, 减小内存占用
# Composition API
Vue3 提出了 Composition API
1
在 Vue2.X 我们使用的是 OptionAPI 里面有我们熟悉的 data
、computed
、methods
、watch
...
在 Vue3
中,我们依旧可以使用 OptionAPI当然不建议 和 Vue3
混用
在 Vue2
中,我们实现一个功能得分到不同的地方,把数据放在 data
,computed
方法放在 methods
里面,分开的太散乱了,几个功能还好,几十个上百个,那就有点...
所以 Vue3
提出了 Composition API ,它可以把 一个逻辑的代码都收集在一起
单独写个hook
,然后再引入,这样就不到处分布,显得很乱了
# setup
setup函数是 Composition API(组合API)的入口
在setup函数中定义的变量和方法最后都是需要 return 出去的 不然无法再模板中使用
<script>
export default {
name: 'App',
setup(){
let name = '流星'
let age = 18
//方法
function say(){
console.log(`我叫${name},今年${age}岁`)
}
//返回一个对象
return {
name,
age,
say
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当然这不是响应式的写法,然后你们可能会问,为什么没有用this
,我们要想一想之前为什么要用this
,还不是作用域的问题,然而这次我们都在setup
里面,所以不会用到this
,而且这里兼容vue2的写法如:data,methods...
,并且在vue2中可以读取到vue3里的配置但是vue3里面不能读取到vue2的配置,所以,vue3和vue2不要混用,如果有重名那么优先setup
。
ps.如果大家不喜欢return
这样的写法的话,可以用vue3新语法糖<script setup>
, <script setup>
就相当于在编译运行是把代码放到了 setup
函数中运行,然后把导出的变量定义到上下文中,并包含在返回的对象中。具体操作可以看掘金其他大佬
script setup基本使用 (opens new window)
上手后才知道 ,Vue3 的 script setup 语法糖是真的爽 (opens new window)
vue3新语法糖——setup script (opens new window)
setup还有几个注意点
- 它比
beforeCreate
和created
这两个生命周期还要快,就是说,setup在beforeCreate,created
前,它里面的this打印出来是undefined
- setup可以接受两个参数,第一个参数是
props
,也就是组件传值,第二个参数是context
,上下文对象,context
里面还有三个很重要的东西attrs
,slots
,emit
,它们就相当于vue2里面的this.$attrs
,this.$slots
,this.$emit
。
通过打印,你可以看到传值,但是会有警告,那是因为我传了两个值,却只接收了一个,要是两个都接收就不会警告了
这个是因为vue3中要求我们用emits
去接收,接收后就不会警告了,但是也可以不理警告直接用
使用插槽时,不能使用 slot="XXX"
,要使用v-slot
,不然会报错
父
<template>
<div class="home">
<HelloWorld wish="不掉发" wishes="变瘦" @carried="carried">
<h3>实现插槽1</h3>
<template v-slot:dome>
<h4>实现插槽2</h4>
</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: 'Home',
components:{
HelloWorld
},
setup(){
function carried(value){
alert(`牛呀,都实现了!!!${value}`)
}
return {
carried
}
}
}
</script>
-------
子
<template>
<h1>HelloWorld</h1>
<h1>{{ wish }}</h1>
<button @click="dream">点击实现</button>
<slot></slot>
<slot name="dome"></slot>
</template>
<script>
export default {
name: "HelloWorld",
props: ["wish",'wishes'],
emits:['carried'],
setup(props,context) {
console.log(props)
console.log(context.attrs)
function dream(){
context.emit('carried',666)
}
return{
dream
}
},
};
</script>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# setup 参数
setup
(props
,context
)
setup` 函数中的第一个参数是 `props`。它接收父组件传递的值,是的就是父子组件信息传递的 `props
第二个参数是 context
里面包含3个属性 { attrs, slots, emit }
,这三个属性大家看名字就应该知道是什么吧 分别对应 this.$attrs
,this.$slots
,this.$emit
attrs
: 除了props
中的其他属性slots
: 父组件传入插槽内容的对象emit
: 和用于父子组件通信
# ref与reactive
# ref
上方我说到,我们写的不是响应式数据,我们写的只是字符串和数字,那怎么变成响应式数据呢,那就呀引入ref,但是如果我们直接在代码里面修改是修改不了的,不如打印一下name和age,你会发现ref把它们变成了对象 并且还是RefImpl
的实例对象
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('燕儿')
let age = ref(18)
console.log(name)
console.log(age)
//方法
function say(){
name='苒苒'
age=20
}
return {
name,
age,
say
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
所以,在修改的时候要.value
去修改,里面还是走的get
与set
去修改页面
其实按道理的话,我们在页面上用的话应该要 name.value
显示的,但是因为vue3
检测到你是ref
对象,它就自动给你.value
了
function say(){
name.value='苒苒'
age.value=20
}
2
3
4
那么要是我定义的ref是个对象呢,因为我们知道尽管ref后会变成RefImpl
的实例对象,所以我们就用XX.value.xx
进行修改
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<h2>职业:{{job.occupation}}</h2>
<h2>薪资:{{job.salary}}</h2>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('燕儿')
let age = ref(18)
let job=ref({
occupation:'程序员',
salary:'10k'
})
console.log(name)
console.log(age)
//方法
function say(){
job.value.salary='12k'
}
return {
name,
age,
job,
say
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
但是我们打印job.value
,你会发现,它不再是RefImpl
实例对象,变成了Proxy
实例对象,他只是vue3底层,把对象都变成了Proxy
实例对象,对于基本数据类型就是按照Object.defineProperty
里面的get
和set
进行数据劫持然后进行响应式,但是如果是对象类型的话,是用到的Proxy
,但是vue3把它封装在新函数reactive里,就相当于,ref中是对象,自动会调用reactive。
reactive只能定义对象类型的响应式数据,前面说到的ref里是对象的话,会自动调用reactive,把Object
转换为Proxy
,那我们来打印一下,你会发现就直接变成了Proxy
,之前为什么会.value
呢,是因为要去获取值,然后通过reactive变成Proxy
,但是现在是直接通过reactive变成Proxy
,而且它是进行的一个深层次的响应式,也可以进行数组的响应式
# ref 获取元素
同样的 ref
还可以用了获取元素
大家在 Vue2.X
中是怎么获取的呢,先在 标签上定义 :ref='XXX'
然后 this.$refs.XXX
来获取
在 Vue3
上获取元素就有些许不同了
1.首先在 模板元素上
ref='XXX'
这里不用v-bind
<template>
<div id="haha" ref="haha"></div>
</template>
2
3
2.在
setup
中
得给 ref
指定类型 HTMLElement
setup() {
let haha = ref<HTMLElement|null>(null)
console.log(haha)
return {
haha,
}
},
2
3
4
5
6
7
8
如果在组件中需要使用到 haha
,就必须把 haha
return
出去合并 data
我们来看看打印的是什么
可以看见的是 haha
是个 Ref
对象,value
值就是我们想要获取到的元素
然后我们可以对 haha
这个 DOM
元素进行操作,比如这个
haha.style.fontSize = '20px'
# reactive
简单写个Vue3响应式例子来说下
Proxy
new Proxy(target, handler)
target
:要使用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)handler
:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p
// 模拟 Vue data
let data = {
msg: '',
age: '',
}
// 模拟 Vue 的一个实例
// Proxy 第一个
let vm = new Proxy(data, {
// get() 获取值
// target 表示需要代理的对象这里指的就是 data
// key 就是对象的 键
get(target, key) {
return target[key]
},
// 设置值
// newValue 是设置的值
set(target, key, newValue) {
// 也先判断下是否和之前的值一样 节省性能
if (target[key] === newValue) return
// 进行设置值
target[key] = newValue
document.querySelector('#app').textContent = target[key]
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
reactive的使用
<template>
<div class="home">
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<h2>职业:{{job.occupation}}<br>薪资:{{job.salary}}</h2>
<h3>爱好:{{hobby[0]}},{{hobby[1]}},{{ hobby[2] }}</h3>
<button @click="say">修改</button>
</div>
</template>
<script>
import {ref,reactive} from 'vue'
export default {
name: 'Home',
setup(){
let name = ref('燕儿')
let age = ref(18)
let job=reactive({
occupation:'程序员',
salary:'10k'
})
let hobby=reactive(['刷剧','吃鸡','睡觉'])
console.log(name)
console.log(age)
//方法
function say(){
job.salary='12k'
hobby[0]='学习'
}
return {
name,
age,
job,
say,
hobby
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
你可能觉得,哎呀,我记不住,我就用ref
,每次就.value
可以了,香香香。他喵的,你正常点,要是一个页面就几个数据的话那还好,要是一堆数据,不得把你.value
点的冒烟吗?,其实你可以按照之前vue2中data的形式来写,这样你就会觉得reactive
香的一批了😁(当然了,这样其实是背弃了vue3的设计理念,如果数据少,业务逻辑不复杂可以这样,如果业务逻辑复杂还是建议根据业务划分,结合自定义hooks......)
<template>
<div class="home">
<h1>姓名:{{_this.name}}</h1>
<h1>年龄:{{_this.age}}</h1>
<h2>职业:{{_this.job.occupation}}<br>薪资:{{_this.job.salary}}</h2>
<h3>爱好:{{_this.hobby[0]}},{{_this.hobby[1]}},{{ _this.hobby[2] }}</h3>
<button @click="say">修改</button>
</div>
</template>
<script>
import {reactive} from 'vue'
export default {
name: 'Home',
setup(){
let _this=reactive({
name:'燕儿',
age:18,
job:{
occupation:'程序员',
salary:'10k'
},
hobby:['刷剧','吃鸡','睡觉']
})
//方法
function say(){
_this.job.salary='12k'
_this.hobby[0]='学习'
}
return {
_this,
say,
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
怎么样,是不是直接暴露出去个_this就好了,这样起码更能理解,不会让人摸不着头脑为什么要.value
,这里建议定义为 _this 或者 _vm,不要定义为data,因为业务和接口返回一般也会定义成data,也不要定义为this,避免和真正的this产生冲突,所有我们最好是直接定义为 _this,相比与vue2,就是多了个下划线,还是符合你之前的习惯的
# ref与reactive的区别
- ref用来定义:基本类型数据。
- ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 - ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive用来定义:对象或数组类型数据。
- reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源代码内部的数据。
- reactive定义的数据:操作数据与读取数据:均不需要
.value
。 当然,我之前就说过,ref可以定义对象或数组的,它只是内部自动调用了reactive来转换。
# vue3的响应式原理
说到vue3的响应式原理,那我们就不得不提一句vue2的响应式了,(狗都知道的一句)通过Object.defineProperty
的get
,set
来进行数据劫持,修改,从而响应式,但是它有什么缺点呢😶
- 由于只有get()、set() 方式,所以只能捕获到属性读取和修改操作,当 新增、删除属性时,捕获不到,导致界面也不会更新。
- 直接通过下标修改数组,界面也不会自动更新。 ok,vue2就聊这么多,什么?你还想听vue2底层?那你就Alt+←,拜拜了您嘞。
对于vue3中的响应式,我们用到的Proxy,当然,我们在vue2里面知道,Proxy是什么,是代理,当然,并不是只用到了它,还有个Window上的内置对象Reflect(反射)
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
const p=new Proxy(data, {
// 读取属性时调用
get (target, propName) {
return Reflect.get(target, propName)
},
//修改属性或添加属性时调用
set (target, propName, value) {
return Reflect.set(target, propName, value)
},
//删除属性时调用
deleteProperty (target, propName) {
return Reflect.deleteProperty(target, propName)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# computed,watch与watchEffect
# computed
在vue3中,把computed
变成为组合式API,那么就意味着你要去引入它,代码如下,一个简易的计算就完成了
<template>
<div class="home">
姓:<input type="text" v-model="names.familyName"><br>
名:<input type="text" v-model="names.lastName"><br>
姓名:{{fullName}}<br>
</div>
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName:'阿',
lastName:'斌'
})
fullName=computed(()=>{
return names.familyName+'.'+names.lastName
})
return {
names,
fullName
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
要是你去修改计算出来的东西,你知道会发生什么吗?警告的意思是计算出来的东西是一个只读属性。
那要是我们想要修改怎么办呢,那么就要用到computed
的终结写法了
<template>
<div class="home">
姓:<input type="text" v-model="names.familyName"><br>
名:<input type="text" v-model="names.lastName"><br>
姓名:<input type="text" v-model="names.fullName"><br>
</div>
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName:'阿',
lastName:'斌'
})
names.fullName=computed({
get(){
return names.familyName+'.'+names.lastName
},
set(value){
let nameList=value.split('.')
names.familyName=nameList[0]
names.lastName=nameList[1]
}
})
return {
names
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
但是,yysy(有一说一),他喵的,我寻思也没有人会去改计算属性吧?如果有,就当我没说😷
# watch
你可能会想到computed
都是组合式API,那么watch
会不会也是组合式API呢?大胆点,它也是, 那么我们就来进行监视
<template>
<div class="home">
<h1>当前数字为:{{num}}</h1>
<button @click="num++">点击数字加一</button>
</div>
</template>
<script>
import {ref,watch} from 'vue'
export default {
name: 'Home',
setup(){
let num=ref('0')
watch(num,(newValue,oldValue)=>{
console.log(`当前数字增加了,${newValue},${oldValue}`)
})
return {
num
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当然这是监听ref
定义出来的单个响应式数据,要是监听多个数据应该怎么办呢?其实可以用多个watch
去进行监听,当然这不是最好的方法,最好的办法其实是监视数组
watch([num,msg],(newValue,oldValue)=>{
console.log('当前改变了',newValue,oldValue)
})
2
3
既然我们监听的是数组,那么我们得到的newValue
和oldValue
也就是数组,那么数组中的第一个就是你监视的第一个参数。
ps.当然之前在vue2中watch
不是有什么其他参数吗,vue3中也有,是写在最后的。
watch([num,msg],(newValue,oldValue)=>{
console.log('当前改变了',newValue,oldValue)
},{immediate:true,deep:true})
2
3
之前我说过,我们现在监听的是监听ref
定义出来数据,那么要是我们监听的是reactive
<template>
<div class="home">
<h1>当前姓名:{{names.familyName}}</h1>
<h1>当前年龄:{{names.age}}</h1>
<h1>当前薪水:{{names.job.salary}}K</h1>
<button @click="names.familyName+='!'">点击加!</button>
<button @click="names.age++">点击加一</button>
<button @click="names.job.salary++">点击薪水加一</button>
</div>
</template>
<script>
import {reactive,watch} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
familyName: '鳌',
age:23,
job:{
salary:10
}
})
watch(names,(newValue,oldValue)=>{
console.log(`names改变了`,newValue,oldValue)
},{deep:false})
return {
names
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
但是你会发现一个问题,为什么newValue
与oldValue
一样呢,就很尴尬,都是新的数据,就算你使用ref
来定义,还是没有办法监听到oldValue
(他喵的,都给你说了ref
定义的对象会自动调用reactive
),所以在监视reactive定义的响应式数据时,oldValue无法正确获取,并且你会发现,它是强制开启深度监视(deep:true
),并且无法关闭。
然而现在我们监视的是reactive
定义的响应式数据的全部属性,是只监听其中的一个属性,那怎么办呢,可能大家会
watch(names.age,(newValue,oldValue)=>{
console.log(`names改变了`,newValue,oldValue)
})
2
3
来进行监视,但是,vue3会警告只能监听reactive
定义的或者ref
定义的,并且不能监听。
那么我们就必须这样写(不会还有人不知道return
可以省略吧?不会吧?不会吧?不会那个人就是你吧?)
watch(()=>names.age,(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
})
2
3
那么要是我们监听的是多个属性,那怎么办呢?emmmm,你正常点,我上面都写了监听多个ref
定义的响应式数据,你就不会举一反三吗?敲代码很累的好吧!!!他喵的,为了防止你们问多个reactive
定义的一个属性,我就只能说和这个是一样的!!!能不能聪明点!!!
watch([()=>names.age,()=>names.familyName],(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
})
2
3
ok,要是我们监听的是深度的属性那要怎么办呢?你会发现我要是只监听第一层是监听不到的,那么我们有两种写法
//第一种
watch(()=> names.job.salary,(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
})
//第二种
watch(()=> names.job,(newValue,oldValue)=>{
console.log('names改变了',newValue,oldValue)
},{deep:true})
2
3
4
5
6
7
8
那么我们就可以这样理解,如果监视的是reactive
定义的响应式数据的属性,并且这个属性是对象,那么我们可以开启深度监视
# watchEffect
watchEffect
是vue3的新函数,它是来和watch
来抢饭碗的,它和watch
是一样的功能,那它有什么优势呢?
- 自动默认开启了
immediate:true
- 用到了谁就监视谁
watchEffect(()=>{
const one = num.value
const tow = person.age
console.log('watchEffect执行了')
})
2
3
4
5
其实吧,watchEffect
有点像computed
,都是里面的值发生了改变就调用一次,但是呢computed
要写返回值,而watchEffect
不用写返回值。
# defineProps & defineEmits
还记得之前vue2的父传子,子改父,的做法吗?使用porps、this.$emit
到了vue3,这种写法改了
我们来看看父组件
<script setup lang="ts">
import { ref } from 'vue'
import Dialog from './Dialog.vue'
const msg = ref('我是msg')
const changeMsg = (val: string) => {
msg.value = val
}
</script>
<template>
// 传进子组件
<Dialog :msg="msg" @changeMsg="changeMsg" />
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
再来看看子组件
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
// 注册父传子的props
const { msg } = defineProps({
msg: {
type: String,
required: true
}
})
// 注册父传子的事件
const emits = defineEmits(['changeMsg'])
const handleClick = () => {
// 修改父组件的值
emits('changeMsg', '修改msg')
}
</script>
<template>
<div @click="handleClick">{{ msg }}</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# defineExpose
这个API主要主要作用是:将子组件的东西暴露给父组件,好让父组件可以使用
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const msg = ref('hello vue3!')
function change() {
msg.value = 'hi vue3!'
console.log(msg.value)
}
// 属性或方法必须暴露出去,父组件才能使用
defineExpose({ msg, change })
</script>
<!-- 父组件 -->
<script setup>
import ChildView from './ChildView.vue'
import { ref, onMounted } from 'vue'
const child = ref(null)
onMounted(() => {
console.log(child.value.msg) // hello vue3!
child.value.change() // hi vue3!
})
</script>
<template>
<ChildView ref="child"></ChildView>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 生命周期
我们先来简单分析下,在vue2中,我们是先
new Vue()
,然后执行beforeCreate
与created
接着问你有没有vm.$mount(el)
,有,才继续执行,但是在vue3中,它是先全部准备好后然后再进行函数。
其实在vue3中生命周期没有多大的改变,只是改变了改变了销毁前,和销毁,让它更加语义化了 beforeDestroy
改名为beforeUnmount
,destroyed
改名为unmounted
然后在vue3中,beforeCreate
与created
并没有组合式API中,setup
就相当于这两个生命周期函数
在vue3中也可以按照之前的生命周期函数那样写,只是要记得有些函数名称发生了改变
Vue2.X
对应Vue3
组合API
Vue2.X | Vue3 | |
---|---|---|
beforeCreate | ---> | setup() |
created | ---> | setup() |
beforeMount | ---> | onBeforeMount |
mounted | ---> | onMounted |
beforeUpdate | ---> | onBeforeUpdate |
updated | ---> | onUpdated |
beforeDestroy | ---> | onBeforeUnmount |
destroyed | ---> | onUnmounted |
activated | ---> | onActivated |
deactivated | ---> | onDeactivated |
errorCaptured | ---> | onErrorCaptured |
onRenderTriggered | ||
onRenderTracked |
# hooks函数
- Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数
- Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数
其实就是代码的复用,可以用到外部的数据,生命钩子函数...,具体怎么用直接看代码,
//一般都是建一个hooks文件夹,都写在里面
import {reactive,onMounted,onBeforeUnmount} 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(event.pageX,event.pageY)
}
//实现鼠标点击获取坐标的方法的生命周期钩子
onMounted(()=>{
window.addEventListener('click',savePoint)
})
onBeforeUnmount(()=>{
window.removeEventListener('click',savePoint)
})
return point
}
//在其他地方调用
import useMousePosition from './hooks/useMousePosition'
let point = useMousePosition()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 自定义hook—useUser
我们要知道Hooks
的好处,其实它就是一个函数,好处有:
- 复用性高
- 减少代码量,提高可维护性
- 相同逻辑可放一起
就比如刚刚我们修改user的那个例子,假如很多页面都需要这个逻辑,那岂不是每个地方都需要写一遍?
所以最好的办法就是把这部分逻辑封装成Hook
注意:规范点,hook都要以
use
开头
// useUser.ts
import { reactive, computed } from 'vue'
const enum Name {
CN = '某亿',
EN = 'coderlibs',
}
const enum Gender {
MAN = '男',
WOMAN = '女',
}
const useUser = () => {
const user = reactive({
name: Name.CN,
gender: Gender.MAN,
})
const userText = computed(() => `我叫${user.name},我是${user.gender}的`)
const switchGender = () => {
user.gender = user.gender === Gender.MAN ? Gender.WOMAN : Gender.MAN
}
return {
switchGender,
userText,
}
}
export default useUser
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
现在我们可以在页面中使用这个自定义hook
<script setup lang="ts">
import useUser from './useUser'
const {
userText,
switchGender
} = useUser()
</script>
<template>
<div @click="switchGender">
{{ userText }}
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# toRef与toRefs
# toRef
toRef
翻译过来其实就是把什么变成ref
类型的数据,可能大家会觉得没有什么用,毕竟我们之前定义时就已经定义成ref
,但是你们想一想,我们在之前是怎么写的
<template>
<div class="home">
<h1>当前姓名:{{names.name}}</h1>
<h1>当前年龄:{{names.age}}</h1>
<h1>当前薪水:{{names.job.salary}}K</h1>
<button @click="names.name+='!'">点击加!</button>
<button @click="names.age++">点击加一</button>
<button @click="names.job.salary++">点击薪水加一</button>
</div>
</template>
<script>
import {reactive} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
name:'老谭',
age:23,
job:{
salary:10
}
})
return {
names
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
是不是一直都是用到代码name.xx
,可能你会说,那我就return
的时候不这样写,改成这样
return {
name:names.name,
age:names.age,
salary:names.job.salary
}
2
3
4
5
但是你要是在页面进行操作时就不是响应式了,为什么呢?那是因为你现在暴露出去的是简简单单的字符串,字符串会有响应式吗?肯定没有呀,但是你要是用到了toRef
,那就是把name.xx
变为响应式,然后操作它时会自动的去修改name
里面的数据
return {
name:toRef(names,'name'),
age:toRef(names,'age'),
salary:toRef(names.job,'salary')
}
2
3
4
5
但是有的人可能会说那我为什么不用ref
去改变?可能大家会发现在页面里也是响应式的,但是我告诉你们,根本用到的不是names
里面的数据,而是你单独定义出来的数据,所以要是这样写,无论怎么修改都不会修改到names
里的数据
return {
name:ref(names.name),
age:ref(names.age),
salary:ref(names.job.salary),
}
2
3
4
5
# toRefs
聪明一点,toRefs
与toRef
有什么不同,加了个s
,toRef
是单个转化为响应式,那toRefs
就是多个转化为响应式咯,这样的话就减少代码,不然要是有成千上万个,那你不是要当憨憨闷写吗?(...
是结构哈,看不懂就麻溜的alt+←),当然它只会结构一层,深层里的代码还是要老实的写
<h1>当前姓名:{{name}}</h1>
<h1>当前薪水:{{job.salary}}K</h1>
return {
...toRefs(names)
}
2
3
4
5
# 自定义指令
还记得vue2中的自定义指令吗?
Vue.directive('xxx', {
// 指令绑定到指定节点,只执行一次
bind() {},
// 指定节点插入dom
inserted() { },
// 节点VNode更新时,可能刚更新,没完全更新
update() {},
// VNode完全更新
componentUpdated() {},
// 指令从指定节点解绑,只执行一次
unbind() {}
})
2
3
4
5
6
7
8
9
10
11
12
再来看看vue3的,比较贴近vue本身的生命周期
app.directive('xxx', {
// 在绑定元素的 attribute 或事件监听器被应用之前调用, 在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用
created() {},
// 当指令第一次绑定到元素并且在挂载父组件之前调用
beforeMount() {},
// 在绑定元素的父组件被挂载后调用
mounted() {},
// 在更新包含组件的 VNode 之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
updated() {},
// 在卸载绑定元素的父组件之前调用
beforeUnmount() {},
// 当指令与元素解除绑定且父组件已卸载时, 只调用一次
unmounted() {},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 其他组合式API
# shallowReactive 与 shallowRef
shallowReactive
浅层次的响应式,它就是只把第一层的数据变为响应式,深层的数据不会变为响应式,shallowRef
如果定义的是基本类型的数据,那么它和ref
是一样的不会有什么改变,但是要是定义的是对象类型的数据,那么它就不会进行响应式,之前我们说过如果ref
定义的是对象,那么它会自动调用reactive
变为Proxy
,但是要是用到的是shallowRef
那么就不会调用reactive
去进行响应式。
shallowReactive:只处理对象最外层属性的响应式(浅响应式)。 shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
let person = shallowReactive({
name:'大理段氏',
age:10,
job:{
salary:20
}
})
let x = shallowRef({
y:0
})
2
3
4
5
6
7
8
9
10
# readonly 与 shallowReadonly
readonly
是接收了一个响应式数据然后重新赋值,返回的数据就不允许修改(深层只读),shallowReadonly
却只是浅层只读(第一层只读,其余层可以进行修改)
names=readonly(names)
names=shallowReadonly(names)
2
# toRaw 与 markRaw
toRaw
其实就是将一个由reactive
生成的响应式对象转为普通对象。如果是ref
定义的话,是没有效果的(包括ref
定义的对象)如果在后续操作中对数据进行了添加的话,添加的数据为响应式数据,当然要是将数据进行markRaw
操作后就不会变为响应式,可能大家会说,不就是和readonly
一样吗?那肯定不一样咯,readonly
是根本没办法改,但markRaw
是不转化为响应式,但是数据还会发生改变。
<template>
<div class="home">
<h1>当前姓名:{{names.name}}</h1>
<h1>当前年龄:{{names.age}}</h1>
<h1>当前薪水:{{names.job.salary}}K</h1>
<h1 v-if="names.girlFriend">女朋友:{{names.girlFriend}}</h1>
<button @click="names.name+='!'">点击加!</button>
<button @click="addAges">点击加一</button>
<button @click="addSalary">点击薪水加一</button>
<button @click="add">添加女朋友</button>
<button @click="addAge">添加女朋友年龄</button>
</div>
</template>
<script>
import {reactive,toRaw,markRaw} from 'vue'
export default {
name: 'Home',
setup(){
let names=reactive({
name:'老伍',
age:23,
job:{
salary:10
}
})
function addAges(){
names.age++
console.log(names)
}
function addSalary(){
let fullName=toRaw(names)
fullName.job.salary++
console.log(fullName)
}
function add(){
let girlFriend={sex:'女',age:40}
names.girlFriend=markRaw(girlFriend)
}
function addAge(){
names.girlFriend.age++
console.log(names.girlFriend.age)
}
return {
names,
add,
addAge,
addAges,
addSalary
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# customRef
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
单纯觉得这个东西的作用只有防抖的作用(要是知道其他的用法可以告知一下我)
<template>
<input type="text" v-model="keyWord">
<h3>{{keyWord}}</h3>
</template>
<script>
import {customRef} from 'vue'
export default {
name: 'App',
setup() {
//自定义一个ref——名为:myRef
function myRef(value,times){
let time
return customRef((track,trigger)=>{
return {
get(){
console.log(`有人从myRef中读取数据了,我把${value}给他了`)
track() //通知Vue追踪value的变化(必须要有,并且必须要在return之前)
return value
},
set(newValue){
console.log(`有人把myRef中数据改为了:${newValue}`)
clearTimeout(time)
time = setTimeout(()=>{
value = newValue
trigger() //通知Vue去重新解析模板(必须要有)
},times)
},
}
})
}
let keyWord = myRef('HelloWorld',1000) //使用自定义的ref
return {keyWord}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
防抖:在第一次触发事件时,不立即执行函数,而是给出一个时间段,如果短时间内大量触发同一事件,只会执行一次函数。
节流:函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活,如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再生效,直至过了这段时间才重新生效。(可以理解为游戏技能冷却期)
如果还是不是很能理解的话去看大佬的 一杯茶的时间🍵,带你彻底学会手写防抖节流 (opens new window)
# provide 与 inject
都知道组件传值吧,在vue2中,如果要在后代组件中使用父组件的数据,那么要一层一层的父子组件传值或者用到vuex
,但是现在,无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
//父
import { provide } from 'vue'
setup(){
let fullname = reactive({name:'阿月',salary:'15k'})
provide('fullname',fullname) //给自己的后代组件传递数据
return {...toRefs(fullname)}
}
//后代
import {inject} from 'vue'
setup(){
let fullname = inject('fullname')
return {fullname}
}
2
3
4
5
6
7
8
9
10
11
12
13
当然子组件也可以用,但是请记住,父子组件传参是有方法的!!!别瞎搞。
# 响应式判断
下面是vue3给的一些判断方法
isRef: 检查值是否为一个 ref 对象。
isReactive:检查对象是否是由 reactive
(opens new window) 创建的响应式代理。
isReadonly: 检查对象是否是由 readonly
(opens new window) 创建的只读代理。
isProxy:检查对象是否是由 reactive
(opens new window) 或 readonly
(opens new window) 创建的 proxy。
import {ref, reactive,readonly,isRef,isReactive,isReadonly,isProxy } from 'vue'
export default {
name:'App',
setup(){
let fullName = reactive({name:'小唐',price:'20k'})
let num = ref(0)
let fullNames = readonly(fullName)
console.log(isRef(num))
console.log(isReactive(fullName))
console.log(isReadonly(fullNames))
console.log(isProxy(fullName))
console.log(isProxy(fullNames))
console.log(isProxy(num))
return {}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 有趣的组件
# Fragment
对我而言这个更像是一种概念,它的意思就相当于创建页面时,给了一个虚拟根标签VNode
,因为我们知道在vue2里面,我们是有根标签这个概念的,但是到来vue3,它是自动给你创建个虚拟根标签VNode
(Fragment
),所以可以不要根标签。好处就是 减少标签层级, 减小内存占用
# Teleport
teleport
提供了一种有趣的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
其实就是可以不考虑你写在什么位置,你可以定义teleport
在任意标签里进行定位等(常见操作为模态框),除了body外,还可以写css选择器(id,class
)
//id定位
<teleport to="#app">
<div class="four">
<div class="five"></div>
</div>
</teleport>
//class定位
<teleport to=".one">
<div class="four">
<div class="five"></div>
</div>
</teleport>
//示例
<template>
<div class="one">
<h1>第一层</h1>
<div class="two">
<h1>第二层</h1>
<div class="three">
<h1>第三层</h1>
<teleport to="body">
<div class="four">
<div class="five"></div>
</div>
</teleport>
</div>
</div>
</div>
</template>
<script>
export default {
name:'App',
setup(){
return {}
}
}
</script>
<style lang="less">
.one{
width: 100%;
background-color: blue;
.two{
margin: 20px;
background-color: aqua;
.three{
margin: 20px;
background-color: aliceblue;
}
}
}
.four{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
.five{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 300px;
height: 300px;
left: 50%;
background-color:#f60;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
打开控制台然后触发事件,观看效果
# Suspense
大家都知道在渲染组件之前进行一些异步请求是很常见的事,suspense
组件提供了一个方案,允许将等待过程提升到组件树中处理,而不是在单个组件中。但是!!!在vue3中特别说明了,Suspense 是一个试验性的新特性,其 API 可能随时会发生变动。特此声明,以便社区能够为当前的实现提供反馈。,我直接尬住,以后估计会发生改变,但是你们愿意卷的话可以去看文章vue3的传送门teleport究竟有多神奇?suspense发起异步请求有多简约? (opens new window),嗯,记得写800字观后感
# vue3其他改动
# router
可能大家会想到路由跳转的问题,可能大家会以为还是用this.$router.push
来进行跳转,但是哦,在vue3中,这些东西是没有的,它是定义了一个vue-router
然后引入的useRoute
,useRouter
相当于vue2的 this.$route
,this.$router
,然后其他之前vue2的操作都可以进行
import {useRouter,useRoute} from "vue-router";
setup(){
const router= useRouter()
const route= useRoute()
function fn(){
this.$router.push('/about')
}
onMounted(()=>{
console.log(route.query.code)
})
return{
fn
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 全局API的转移
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 |
# 其他改变
移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes
移除v-on.native
修饰符
移除过滤器(filter
)
...
对了有手痒的想实战项目的话可以去看一看大佬的🎉🎉Vue 3 + Element Plus + Vite 2 的后台管理系统开源啦🎉🎉 (opens new window)
# 🎉🎉完结撒花🎉🎉
← Vue2 进阶 Vue-Router →