vue3.0
一、Vue3带来了什么
### 1.性能的提升
- 打包大小减少41%
- 初次渲染快55%, 更新渲染快133%
- 内存减少54%
......
### 2.源码的升级
- 使用Proxy代替defineProperty实现响应式
- 重写虚拟DOM的实现(对比更快)和Tree-Shaking
......
### 3.拥抱TypeScript(重点)
- Vue3可以更好的支持TypeScript
### 4.新的特性(重点)
1. Composition API(组合API)
- setup配置
- ref与reactive
- watch与watchEffect
- provide与inject
- ......
2. 新的内置组件
- Fragment
- Teleport
- Suspense
3. 其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除keyCode支持作为 v-on 的修饰符
- ......
二、创建Vue3.0工程
## 1.使用 vue-cli 创建
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上(注)
vue --version / vue -V (安装或者升级你的@vue/cli: npm install -g @vue/cli )
## 创建,再选择vue3版本即可
vue create vue_test
## 2.使用 vite 创建
- 什么是vite?—— 新一代前端构建工具(还未大规模应用)。(旧:webpack)
- 优势如下:
- 开发环境中,无需打包操作,可快速的冷启动。
- 轻量快速的热重载(HMR)。
- 真正的按需编译,不再等待整个应用编译完成。(启动速度比脚手架明显快)
- 传统构建 与 vite构建对比图
## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev
三、工程化结构分析
以vue3-cli为例:结构和vue2-cli一样,只是里面写法不同:主要分析src文件夹
1.src/main.js:
//引入的不再是Vue构造函数,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”,打印app对象可以看到属性比vm少)
const app = createApp(App)
app.mount('#app')
---
(相当于:实例-> App.vue ->各种组件)
写法与vue2对比:
import Vue from 'vue' //vue3工程里面引入不到Vue构造函数
import App from './App.vue'
const vm = new Vue({
render:h => h(App) //相当于(h) =>{return h(App) } ,h是渲染函数
})
vm.$mount('#app')
2.App.vue:
<template>
<!-- Vue3组件中的模板结构可以没有根标签 -->
<img alt="Vue logo" src="./assets/logo.png">
</template>
<script>
export default {
name: 'App',
}
</script>
3.vue开发者工具,需要也用能同时兼容vue3的,可以打开谷歌商城在线下载。
edge浏览器上面下载的可以同时兼容vue2和3
四、常用 Composition API (组合)
## 1.拉开序幕的setup
1. 理解:Vue3.0中一个新的配置项,值为一个函数,简写:setup(){...}。
2. 组件中所用到的:数据、方法等等,均要配置在setup中。
3. setup函数的两种返回值:
1.若返回一个对象,则对象中的属性、方法, 在模板(页面)中均可以直接使用。(重点)
2.若返回一个渲染函数:则可以自定义渲染内容,以自定义的内容代替模板页面(了解)
4. 注意点:
1. 尽量不要与Vue2.x配置混用(了解)
- Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。
- 但在setup中不能访问到Vue2.x配置(data、methos、computed...)。
- 如果有重名, setup优先。
2. setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(注)
代码:
(了解:vue2是直接写data、method,vue3是将其写在setup函数里面,数据不需要data,方法不需要method,但是vue3里面也可以写vue2的方式,向下兼容)
app.vue:
<template>
<h1>一个人的信息</h1>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="sayHello">说话(Vue3所配置的——sayHello)</button>
</template>
<script>
// import {h} from 'vue' 引入渲染函数
export default {
name: 'App',
setup(){
//数据,/此处只是测试一下setup,暂时不考虑响应式的问题。
let name = '张三'
let age = 18
let a = 200
//方法
function sayHello(){
alert(`我叫${name},我${age}岁了,你好啊!`)
}
//返回一个对象(常用)
return {
name,
age,
sayHello,
}
//返回一个函数(渲染函数),内容会代替页面模板template里的内容
//return ()=>{return h('h1','111') } //h1标签,内容为'111'
}
}
</script>
## 2.ref函数
(vue2的ref是来标识一个组件/标签元素的,vue3里面ref是一个函数)
- 作用: 定义一个响应式的数据
- 语法: `const xxx = ref(initValue)` (拓展:const定义的对象,可以修改属性)
- 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)
- JS中操作数据: `xxx.value` (注)
- 模板中读取数据: 不需要.value,直接:`<div>{{xxx}}</div>`
- 特殊:Ref定义了数组\对象,除了对象在页面中不用.value,其余都要.value。
- 备注:
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠`Object.defineProperty()`的`get`与`set`完成的。(注)
- 对象类型的数据:内部求助了Vue3.0中的一个新函数—— `reactive`函数。(该函数封装了proxy)
代码:
<template>
<h1>一个人的信息</h1>
<h2>姓名:{{name}}</h2>
<h3>工作种类:{{job.type}}</h3>
<h3>工作薪水:{{job.salary}}</h3>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'App',
setup(){
//数据
// let name = '张三'
let name = ref('张三') //将字符串修饰成一个引用对象refImpl,变成响应式,结构见图(类似vue2的响应式)
let job = ref({ (与实践冲突)//对象修饰后也是一个引用对象refImpl,但是value是一个proxy对象,proxy里面可以直接调属性
type:'前端工程师',
salary:'30K'
})
//方法
function changeInfo(){
//name= '李四' ; 不使用ref时,直接修改数据,页面不会响应。
name.value = '李四' //这样修改才能同时响应页面;而在页面修改数据,template中不需要name.value
console.log(job.value)
job.value.type = 'UI设计师' //无论对象是否嵌套,属性都会被监视
job.value.salary = '60K'
}
//返回一个对象(常用)
return {
name,
age,
job,
changeInfo
}
}
}
</script>
## 3.reactive函数
- 作用: 定义一个对象类型的响应式数据(基本类型不要用它,用不了,要用ref函数)
- 语法:`const 代理对象= reactive(源对象),接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
- reactive定义的响应式数据是“深层次的”。
- 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
代码:
<template>
<h1>一个人的信息</h1>
<h2>姓名:{{person.name}}</h2>
...
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import {reactive} from 'vue'
export default {
name: 'App',
setup(){
//数据
let person = reactive({
name:'张三',
age:18,
job:{
type:'前端工程师',
salary:'30K',
a:{
b:{
c:666
}
}
},
hobby:['抽烟','喝酒','烫头']
})
//不再需要对象.vaule.属性,reactive传入对象比ref传入对象调用属性方便,也有响应式。(注)
function changeInfo(){
person.name = '李四'
person.age = 48
...
person.job.a.b.c = 999
person.hobby[0] = '学习' //vue2里面直接索引修改数组无法响应式,reactive可以。
}
return {
person,
changeInfo
}
}
}
</script>
## 4.Vue3.0中的响应式原理
### vue2.x的响应式
- 实现原理:
- 对象类型:通过`Object.defineProperty()`对属性的读取、修改进行拦截(数据劫持)。
- 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
- 存在问题:
- 新增属性、删除属性, 界面不会更新。 (注意:get/set,只能检测属性值读取、设置)
- 直接通过下标修改数组, 界面不会自动更新。
(但是也有解决方法,如this.$set等)
代码:
<template>
<div>
<h2 v-show="person.name">姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2 v-show="person.sex">性别:{{person.sex}}</h2>
<h2>性别{{person.sex}}</h2>
<button@click="addSex">添加一个sex属性</button>
<button @click="deleteName">删除name属性</button>
</div>
</template>
<script>
import Vue from 'vue'
export default {
name: 'App',
data(){
return {
person:{
name:'张三',
age:18,
hobby:['学习',吃饭']
}
}
}
methods:{
addSex(){
this.person.sex ='女' //追加属性,无响应式效果
this.$set(this.person,'sex','女')
Vue.set(this.person,'sex','女')
},
deleteName(){
delete this.person.name //无响应式效果,数据属性没了,页面不会更新
this.$delete(this.person,'name') (记)
Vue.delete(this.person,'name','女')
},
updateHobby(){
this.person.hobby[0]='逛街' //无响应式效果
this.$set(this.person.hobby,0,'诞街')
Vue.set(this.person.hobby,0,'诞街')
this.person.hobby.splice(0,1,'逛街')
}
}
}
### Vue3.0的响应式
- 实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。(reactive(对象)即如此)
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
代码:
//源数据
let person = {
name:'张三',
age:18
}
// 1.模拟Vue2中实现响应式,此时删除、增加属性不能检测到
let p = {}
Object.defineProperty(p,'name',{
configurable:true,(可配置的)
get(){ //有人读取name时调用
return person.name
},
set(value){ //有人修改name时调用
console.log('有人修改了name属性,我发现了,我要去更新界面!')
person.name = value (此处若p.name = value 会陷入死循环)
}
})
//2. 模拟Vue3中实现响应式(Proxy意思为代理,无论跨域代理 还是数据代理都是此含义)
const p = new Proxy(person,{}) //此最简单的方式不能捕获读、改、增、删,作出响应,
但是p已经完全代理person,即修改p与修改person对象一致
const p=new Proxy( person ,{ //配置这些为了重写功能,变成响应式(即同时响应到页面)
//有人读取p的某个属性时调用
get(target,propName){ //target指的就是person
console.log(`有人读取了p身上的${propName}属性`)
// return target[propName];
return Reflect.get(target,propName)
}
//有人修改p的某个属性、或给p追加某个属性时调用(新,defineProperty没有的)
set(target,propName,value){
console.log(`有人修改了p身上的$(propName)属性,我要去更新界面了!`)
// target[propName] = value ,与下面的一样,都是直接操作person,不操作p,避免了死循环
Reflect.set(target,propName,value)
},
//有人删除p的某个属性时调用(新)
deleteProperty(target,propName){
console.log(`有人删除了p身上的$(propName)属性,我要去更新界面了!)
// return delete target[propName]
return Reflect.deleteProperty(target,propName)
}
})
-window.Reflect(反射) :es6新增,反射对象,Object身上很多api在慢慢复制到其身上。
Reflect.get(obj,'a')
Reflect.set(obj,'a',100)
Reflect.deleteProperty(obj,'a')
以上也能修改全局的某个对象
下面举例子体现Reflect的优势:(了解)
let obj = {a:1,b:2}
//通过Object.defineProperty去操作,框架代码中报错会停止执行,需要try catch
//#region
/* try {
Object.defineProperty(obj,'c',{
get(){
return 3
}
})
Object.defineProperty(obj,'c',{ //不处理异常,重复代理相同属性到对象上会报错。
get(){
return 4
}
})
} catch (error) {
console.log(error)
} */
//#endregion
//通过Reflect.defineProperty去操作,框架代码中报错不会停止执行,并且少了try catch
//#region
/* const x1 = Reflect.defineProperty(obj,'c',{
get(){
return 3
}
})
console.log(x1)
const x2 = Reflect.defineProperty(obj,'c',{
get(){
return 4
}
})
if(x2){
console.log('某某某操作成功了!')
}else{
console.log('某某某操作失败了!')
} */
//#endregion
## 5.reactive对比ref
- 从定义数据角度对比:
- ref用来定义:基本类型数据。
- reactive用来定义:对象(或数组)类型数据。
- 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过`reactive`转为代理对象。(但是第一层仍然是ref引用对象形式,而.value里面是proxy)
- 从原理角度对比:
- ref通过`Object.defineProperty()`的`get`与`set`来实现响应式(数据劫持)。
- reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
- 从使用角度对比:
- ref定义的数据:js中操作数据需要`.value`,读取数据时模板中直接读取不需要`.value`。
- reactive定义的数据:操作数据与读取数据:均不需要`.value`。
## 6.setup的两个注意点
#vue2补充知识点$attrs、$slot: (了解)
1.vue2的父传子prop值,传个子组件的某个属性没有在prop声明,vc的$attrs会获取该属性,但是$attrs不能做类型限制,并且给页面直接调用写法麻烦。
2.插槽:当父组件在子组件标签之间传入内容时,vc的$slot可以看到传入的标签。
-父组件:
<template>
<div class="app">
<h1>我是Vue2写的效果</h1>
<Demo msg="你好啊" school="11">
<span>11</span>
<span>11!!!</span>
<template slot="test1">//具体名插槽
<span>222</span>
</template>
<template slot="test2>
<span>222</span>
</template>
</Demo>
</div>
</template>
<script>
import Demo from './components/Demo'
export default {
name: 'App',
components:{Demo}
}
</script>
-子组件Demo:
<template>
<div class="demo"
<h2>我是Demo组件</h2>
<slot></slot> //插入位置
<slot name="test1">我是一些默认值1</slot>
<slot name="test2">我是一些默认值2</slot>
</div>
</template>
<script>
export default {
name:'Demo',
props:['msg'],
mounted(){
console.log(this)
},
}
</script>
# setup执行的时机
- 在beforeCreate之前执行一次(即更早),this是undefined。 (特别注意, setup没有this,由于执行太早)
# setup的参数
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 `this.$attrs`。
- slots: 收到的插槽内容, 相当于 `this.$slots`。
- emit: 分发自定义事件的函数, 相当于 `this.$emit`的函数。
代码:
-子组件:
<template>
<h1>一个人的信息</h1>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<button @click="test">测试触发一下Demo组件的Hello事件</button>
</template>
(其实就是setup里面没有this,围绕这个开展,完成vue2原有的操作)
<script>
import {reactive} from 'vue'
export default {
name: 'Demo',
beforeCreate(){
console.log("beforeCreate不是最早执行,而是setup最早");
},
props:['msg','school'], //父组件传了没接收会有警告,声明了没传没有警告而是undefined。(vue3是如此,vue2情况待确定)
emits:['hello'], //自定义事件需接收 (vue3特有,因为vue2中父给子传事件,是直接绑到子组件标签上,子组件再this.$emit调用;
vue3绑定是一样的,但是调用时setup没有this,所以需在此声明接收,后面再setup的参数触发)
setup(props,context){
console.log("setup最早执行");
console.log('---setup---',props) //具有proxy对象的props属性
console.log('---setup---',context) //普通对象,有attrs、slot、emit
console.log('---setup---',context.attrs) //相当与Vue2中的$attrs,只有props没有接收的属性才会到这里
console.log('---setup---',context.emit('xx')) //触发自定义事件的。
console.log('---setup---',context.slots) //插槽$slot
//数据
let person = reactive({
name:'张三',
age:18
}),
//方法
function test(){
context.emit('hello',666)
}
//返回一个对象(常用)
return {
person,
test
}
}
}
</script>
-父组件:
<template>
<Demo @hello="showHelloMsg" msg="你好啊" school="333">
<template v-slot:qwe> (vue2里具体名插槽:slot='xx',v-slot:xxx都可以;但是vue3兼容最好使用v-slot:xx) (注意:vue3弃用slot='插槽名称')
<span>333</span>
</template>
</Demo>
</template>
<script>
import Demo from './components/Demo'
export default {
name: 'App',
components:{Demo},
setup(){
...
}
}
</script>
- 使用setup语法糖的时候获取contenxt的值
useAttrs 方法 获取 attrs 属性
useSlots 方法获取 slots 插槽
defineEmits 方法获取 emit 自定义事件
另外,如父组件传了值,子组件未接收,会报警告,即使用了useAttrs()。 (开发中注意)
此时只需在页面中使用{{$attrs}}或者useAttrs()返回的变量即可。
获取默认插槽:
const slots = useSlots()
const defaultSlot = slots.default() //数组
## 7.计算属性与监视 (都是组合式api,需引入)
### 1.computed函数(与vue2的区别,不写成属性形式computed:{},而是函数调用的形式)
<template>
...
</template>
<script>
import {reactive,computed} from 'vue'
export default {
name: 'Demo',
setup(){
//数据
let person = reactive({
firstName:'张',
lastName:'三'
})
let fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
//为了语义化更好,直接追加到person对象,有响应式;计算属性——简写(没有考虑计算属性被修改的情况)
person.fullName = computed(()=>{ (注意,简写只需考虑读,这里是个箭头函数)
return person.firstName + '-' + person.lastName
})
//计算属性——完整写法(考虑读取与修改,vue2也有)
person.fullName = computed({ (注意,完整写法考虑读和写,传的是个配置对象)
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
return {
person
}
}
}
</script>
注意:
- computed属性的调用(WritableComputedRef类型):页面可以直接获取需要.value,否则计算属性return的是一个对象/数组时,会有类型问题;js中要获取也需.value。
(特别注意)
- 配置对象写法时,若计算属性return的是一个对象/数组,后续再修改,则触发不了set函数。若是追加属性,会直接设置,并且有响应式;
若是整个对象替换,会丢失响应式。且此时若需要对象的某个属性X++,只能完整写法X=X+1(特殊)
解释:
因为在Vue 3中,计算属性的实现方式已经发生了变化。在Vue 2中,计算属性是通过Object.defineProperty()方法实现的,
而在Vue 3中,计算属性是通过Proxy实现的。当计算属性返回的是一个对象时,Proxy会将这个对象转换为响应式对象,
从而使得这个对象的属性变化可以被Vue追踪到并触发更新,而不需要再通过Set回调来实现,同时也解释了追加属性有响应式、替换整个对象没有。
### 2.watch函数 (与vue2的区别,不写成属性显示watch:{},而是函数调用的形式 ;更好的一对多,多对一变化;并且reactive数据检测会出现的情况较多)
<template>
...
</template>
<script>
import { ref, reactive , watch } from 'vue' //组合api的体现
export default {
name: 'Demo',
setup(){
let sum = ref(0)
let msg = ref('你好啊')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
* 监测ref属性:
-情况一:监视ref所定义的一个响应式数据
watch(sum,(newValue,oldValue)=>{ //不用.value,sum.value相当于值0
console.log('sum变了',newValue,oldValue) //直接拿到值,不需要再newValue.value
},{immediate:true})
-情况二:监视ref所定义的多个响应式数据(即多个属性变化产生一个效应)
watch([sum,msg],(newValue,oldValue)=>{ //此时newValue是数组[],两个元素为监测的两个变量
console.log('sum或msg变了',newValue,oldValue)
},{immediate:true})
监测ref属性注意点:
setup(){
let sum = ref(0)
let msg = ref('你好啊')
let person = ref({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
下面是ref加工对象数据,了解即可:
watch(person,(newValue,oldValue)=>{ //不能检测到person变化,特殊
console.log('person的值变化了',newValue,oldValue)
},)
watch(person.value,(newValue,oldValue)=>{ //方法一,检测refImpi对象的value(proxy对象)
console.log('person的值变化了',newValue,oldValue)
},)
watch(person,(newValue,oldValue)=>{ //方法二,深度监听,因为refImpi对象的第一层属性值都没有变化(value地址值不变)
console.log('person的值变化了',newValue,oldValue)
},{deep:true})
上面两个的oldValue都是无效的,与newValue一致。
* 监测reactive属性:
-情况三:监视reactive所定义的一个响应式数据的全部属性(相当于监测proxy对象,会带来下面两个问题)
1.注意:此处无法正确的获取oldValue;用了ref也一样,因为ref对象的value属性里面也是一个用reactive处理的proxy对象。
若一定需要用到oldValue:用ref单独拿出该属性,再追加给person对象
2.注意:默认强制开启了深度监视(deep配置无效)
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:false}) //此处的deep配置无效
-情况四:监视reactive所定义的一个响应式数据中的某个属性 (监测指定对象某个属性写法,此处相当于监测普通类型,oldValue有效)
//watch(person.name,(newValue,oldValue)=>{ //直接person.name会报警告,需箭头函数返回,特殊
watch(()=>person.name,(newValue,oldValue)=>{
console.log('person的name变化了',newValue,oldValue)
})
-情况五:监视reactive所定义的一个响应式数据中的多个属性 (监测指定对象几个特定属性写法)
watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{
console.log('person的name或age变化了',newValue,oldValue)
})
-特殊情况
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue) //此时不开启deep配置,多层属性检测不到
},{deep:true}) //此处由于监视的是reactive定义的proxy对象中的某个属性,且属性是对象(特殊),
所以deep配置有效;oldValue也拿不到(因为也是对象) */
监测对象总结:如果检测的具体内容是一个对象,那么oldValue会获取不到。如果检测的是reactive定义所产生的proxy对象,会强制深度监测。
}
return {
sum,
msg,
person
}
}
}
</script>
### 3.watchEffect函数
- watch的套路是:既要指明监视的属性,也要指明监视的回调。
- watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
- watchEffect有点像computed:
- 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
<script>
import {ref,reactive,watchEffect} from 'vue'
export default {
name: 'Demo',
setup(){
let sum = ref(0)
let msg = ref('你好啊')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
//watchEffect初始化调用一次,所指定的回调中用到的数据只要发生变化,则也会重新执行回调。
(像没有返回值的计算属性。而计算属性实现这种两个变量有变化,就执行回调的功能,需要有返回值,且被页面引用)
watchEffect(()=>{
const x1 = sum.value
const x2 = person.job.j1.salary
console.log('watchEffect所指定的回调执行了')
})
return {
sum,
msg,
person
}
}
}
</script>
## 8.生命周期
不同点:
1.vue3销毁是unmounted
2.vue3的app(vm)较轻量,且当el或者mount指定容器后,才会执行beforeCreaed()和created();vue2会执行这两个之后才判断有没有指定容器。(注)
3.生命周期普通写法(即按vue2写法)和组合式API写法(写在setup里),都可以用。
- Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有两个被更名:
- ```beforeDestroy```改名为 ```beforeUnmount```
- ```destroyed```改名为 ```unmounted```
- Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
- `beforeCreate`===>`setup()`
- `created`=======>`setup()`
- `beforeMount` ===>`onBeforeMount`
- `mounted`=======>`onMounted`
- `beforeUpdate`===>`onBeforeUpdate`
- `updated` =======>`onUpdated`
- `beforeUnmount` ==>`onBeforeUnmount`
- `unmounted` =====>`onUnmounted`
(其实就是将create的两个合到setup,然后其它生命周期写在setup中,并且名字加on;然后vue3最后两个函数更名) (注)
注意vue3是写在setup里面,组合式API用法,并且是函数调用形式,传箭头函数。如onMounted(()=>{})
## 9.自定义hook函数
- 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
- 类似于vue2.x中的mixin。
- 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
例子:(详细见例子)
hooks文件夹的usePoint.js:
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
}
页面:
<template>
...
</template>
<script>
import {ref} from 'vue'
import usePoint from '../hooks/usePoint'
export default {
name: 'Demo',
setup(){
let point = usePoint()
return {point}
}
}
</script>
hook函数的作用:其实就是将一个功能的数据、方法、生命周期函数,都封装出去到自定义的js文件中,
该文件暴露一个函数,函数返回值为该功能需要的数据;要复用时,只需引入该功能函数赋值数据即可。
hook函数调用后返回的取决于函数,一般是普通对象包裹响应式数据。
## 10.toRef(为了页面有对象数据层次较深时,调用方便些)
ref:定义响应式数据,基本、对象数据类型。(toRef与ref没有直接关系)
- 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
- 语法:```const name = toRef(person,'name')```
- 应用: 要将响应式对象中的某个属性单独提供给外部使用时,减少调用麻烦。
- 扩展:```toRefs``` 与```toRef```功能一致,但可以批量创建多个 ref 对象,语法:```toRefs(person)```
<template>
<h4>{{person}}</h4>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪资:{{job.j1.salary}}K</h2>
<button @click="name+='~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">涨薪</button>
</template>
<script>
import {ref,reactive,toRef,toRefs} from 'vue'
export default {
name: 'Demo',
setup(){
//数据
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
//返回一个对象(常用)
return {
person,
//下面三个写法原意是为模版中简写变量,但是这样只能一开始显示,会丢失响应式,因为没改到对象的堆内容的东西,相当于返回死的数据给页面。
name:person.name,
age:person.age,
salary:person.job.j1.salary,
//于是出现了toRef,将变量改为ref对象,可以使页面方便的读取到对象的某个属性值,并且能和原对象建立响应式的关联,即回显和修改都相当于操作person。
// name:toRef(person,'name'),
// salary:toRef(person.job.j1,'salary'), //多层时,该方式的简写优势
//toRefs将整个对象直接改造,只能改造对象的第一层,返回一个新对象,将其解构后页面能直接使用第一层,对象里的属性调用与修改都相当于操作person
但是需要第一层.value
...toRefs(person)
}
}
}
</script>
# 三、其它 Composition API (相对少用)
1.shallowReactive 与 shallowRef
- shallowReactive:只处理对象最外层属性的响应式(浅响应式)。
- shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
- 什么时候使用?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是只渲染或者直接用新的对象替换 ===> shallowRef。
<template>
<h4>当前的x.y值是:{{x.y}}</h4>
<button @click="x={y:888}">点我替换x</button> (有效)
<button @click="x.y++">点我x.y++</button> (无效)
<hr>
<h4>{{person}}</h4>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪资:{{job.j1.salary}}K</h2>
<button @click="name+='~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">涨薪</button> (无效)
</template>
<script>
import {ref,reactive,toRef,toRefs,shallowReactive,shallowRef} from 'vue'
export default {
name: 'Demo',
setup(){
let person = shallowReactive({ //只考虑第一层数据的响应式
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
shallowRef内容是基本数据类型时,与ref效果一样,但是对象类型时不一样,传入对象时区别:
1.ref和shallowRef都会转为refImpl对象,但是里面有属性时,ref会求助reactive将该属性转为proxy,shallowRef不会。
2.shallowRef传入对象没有响应式,但是可以直接新对象赋值替换。
let x = shallowRef({
y:0
})
return {
x,
person
}
}
}
</script>
2.readonly 与 shallowReadonly
- readonly: 让一个响应式数据变为只读的。
- shallowReadonly:让一个响应式数据第一层变为只读的(浅只读)。
- 应用场景: 不希望数据被修改时。
import {ref,reactive,readonly,shallowReadonly} from 'vue'
let sum = ref(0)
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
//加工数据后,覆盖要暴露的变量。
person = readonly(person)
person = shallowReadonly(person) //只是第一层属性只读,深层次的依然可以修改
sum = readonly(sum)
sum = shallowReadonly(sum) //不是对象,此时与readonly没区别
补充:
如果直接去掉ref/reactive,将数据写死,此时仍然可以修改数据,只是页面上一样不会有变化,若希望数据源不被修改,最好用此api。
3.toRaw 与 markRaw(只处理reartive数据)
背景:
普通数据->响应式数据(ref/reactive)
普通数据<-响应式数据
- toRaw:
- 作用:将一个由`reactive`生成的响应式对象转为普通对象。
- 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- markRaw:(应用场景较广)
- 作用:标记一个对象,使其永远不会再成为响应式对象。
- 应用场景:
1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
function showRawPerson(){
const p = toRaw(person) //将该数据从 响应式数据->普通数据,返回变量 (只能用于reartive数据)
p.age++ //该变量可以修改,页面不会响应
}
function addCar(){
let car = {name:'奔驰',price:40}
person.car = car //proxy对象的响应式追加属性,该属性及其内容也会是响应式的。(vue2中响应式直接追加对象,不会有响应式)
person.car = markRaw(car) //限定添加的属性不做响应式。
}
4.customRef
- 作用:创建一个自定义的 ref,能够自定义添加ref的get/set触发时的功能。(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,delay){
let timer
return customRef((track,trigger)=>{ //第一个return是将customRef函数的值返回,第二个return是语法要求。
return {
get(){
track() //通知Vue追踪value的变化,后面set里改变value和模版更新时,才会重新调get(),否则数据改变 视图仍不变(特殊)
return value
},
set(newValue){
clearTimeout(timer) //定时器实现延迟更新,只保留最后一个延时器实现防抖。
timer = setTimeout(()=>{
value = newValue
trigger() //通知Vue去重新解析模板
},delay)
},
}
})
}
let keyWord = myRef('hello',500) //使用程序员自定义的ref,能实现修改数据后指定秒数后再更新数据
return {keyWord}
}
}
</script>
5.provide 与 inject(添加)
- 作用:实现祖与后代组件间通信
- 套路:父组件有一个`provide`选项来提供数据,后代组件有一个`inject`选项来开始使用这些数据(子/孙组件都可以接收获取,只是传子一般用props简单)
-具体写法:(provide 与 inject:使用前都是有组合式API的引入)
1. 祖组件中:
```js
setup(){
......
let car = reactive({name:'奔驰',price:'40万'})
provide('car',car) //第一个参数是自定义的名称
......
}
```
2. 后代组件中:
```js
setup(props,context){
......
const car = inject('car')
return {car}
......
}
```
6.响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 `reactive` 创建的响应式代理
- isReadonly: 检查一个对象是否是由 `readonly` 创建的只读代理
- isProxy: 检查一个对象是否是由reactive/readonly创建的代理(准确的说,判断其类型是不是代理对象proxy,readonly只是加工了reactive,没有改变其类型)
<script>
import {ref, reactive,toRefs,readonly,isRef,isReactive,isReadonly,isProxy } from 'vue'
export default {
name:'App',
setup(){
let car = reactive({name:'奔驰',price:'40W'})
let sum = ref(0)
let car2 = readonly(car)
//全部是true
console.log(isRef(sum))
console.log(isReactive(car))
console.log(isReadonly(car2))
console.log(isProxy(car))
return {...toRefs(car)}
}
}
</script>
# 四、Composition API 的优势
1.vue2的Options API 存在的问题(配置式的api)
使用传统OptionsAPI中,一个功能涉及到的内容分散在数据、方法、计算属性等等,后期维护就需要分别在data,methods里零散修改 。
2.Composition API 的优势
将一个功能相关的数据、方法、配置、相关的生命周期钩子等相关的内容都抽离到一起,不仅在页面引入该功能方便,维护该功能也方便。(需借助hook)
# 五、新的组件
## 1.Fragment
- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
## 2.Teleport(传送)
- Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
- Teleport传送的内容是到目标选择器之下。还可以disabled 属性接收一个 Boolean 值,true 代表不允许传送,false 代表传送
场景:当页面多层嵌套时,当孙组件中出现弹框,需要相对整个页面开启绝对定位时,很难控制html、body标签为其包含块。
孙组件:
<template>
<div>
<button @click="isShow = true">点我弹个窗</button>
<teleport to="body"> //或者接任意css选择器
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<h4>一些内容</h4>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name:'Dialog',
setup(){
let isShow = ref(false)
return {isShow}
}
}
</script>
<style>
.mask{
position: absolute; // html根元素为默认初始包含块,当传送到body下时,绝对定位偏移量相对根标签。
top: 0;bottom: 0;left: 0;right: 0; //能实现宽高撑满屏幕(注意,绝对布局公式,待实验)
background-color: rgba(0, 0, 0, 0.5);
}
.dialog{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
text-align: center;
width: 300px;
height: 300px;
background-color: green;
}
</style>
## 3.Suspense(悬念,处于实验阶段)
- 使组件异步加载时,等待时能显示其它提示内容,更好的用户体验
- 使用步骤:
-使用Suspense标签包裹子组件,并配置好Suspense的插槽`default` 与 `fallback`
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default> //名字不能改
<Child/>
</template>
<template v-slot:fallback>
<h3>稍等,加载中...</h3>
</template>
</Suspense>
</div>
</template>
<script>
// import Child from './components/Child' //静态引入。页面加载的时候,如果该子组件没加载完,整个页面都等待着<child>标签。
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child')) //(动态)异步引入,会在页面加载完后再出现,比页面的同步加载晚。
export default {
name:'App',
components:{Child},
}
</script>
子组件:
<template>
<div class="child">
<h3>我是Child组件</h3>
{{sum}}
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name:'Child',
async setup(){ //前面说setup不能返回一个promise,因为那样模版会拿不到定义的数据;但是此处可以,因为该组件嵌套在<Suspense>组件里。(特殊)
let sum = ref(0)
let p = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve({sum})
},3000) //3秒后渲染该组件
})
return await p
}
}
</script>
# 六、其他
## 1.全局API的转移
- Vue 2.x 有许多全局 API和配置, Vue3.0中对这些API做出了调整:
- 将全局的API,即:Vue.xxx调整到应用实例(app)上
| 2.x 全局 API(`Vue`) | 3.x 实例 API (`app`) |
| ------------------------- | ------------------------------------------- |
| Vue.config.xxxx | app.config.xxxx |
| Vue.config.productionTip | 移除 (vue3自动去掉生产提示)
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
| Vue.prototype | app.config.globalProperties (注) |
## 2.其他改变
- data选项应始终被声明为一个函数。 (vue2中, 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(自定义按键编码)。(原因:兼容性较差)
(实际开发 @keyup.enter.native="xxxx" 依然有效)
- (注意)移除`v-on.native`修饰符,原来vue2里面,会出现子组件标签@click='xxx'时,vue分不清是自定义事件还是原生事件,所以需要@click.native='xxx'。
- 父组件中绑定事件
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
- 子组件中声明自定义事件
<script>
export default {
emits: ['close'] //vue3的解决方案:只要子组件没有接收的事件,就认为是原生事件
}
</script>
小结:
在vue3框架click、change(这类原生DOM事件),不管是在标签、组件上都是原生DOM事件,只要没有emits接收。
vue2中却不是这样的,在vue2中组件标签需要通过native修饰符才能变为原生DOM事件。
- 移除过滤器(filter)
建议用方法调用或计算属性去替换过滤器。