一、Vue3快速上手
1.Vue3简介
- 2020年9月18日,Vue.js发布3.0版本,代号:One Piece(海贼王)
- 耗时2年多、 2600+次提交、 30+个RFC、 600+次PR、 99位贡献者
- github上的tags地址:https://github.com/vuejs/vue-next/releases/tag/v3.0.0
2.Vue3带来了什么
2.1 性能的提升
- 打包大小减少41%
- 初次渲染快55%, 更新渲染快133%
- 内存减少54% …
2.2 源码的升级
- 使用Proxy代替defineProperty实现响应式
- 重写虚拟DOM的实现和Tree-Shaking …
2.3 拥抱TypeScript
- Vue3可以更好的支持TypeScript
2.4 新的特性
- Composition API(组合API)
setup配置
ref与reactive
watch与watchEffect
provide与inject
…
- 新的内置组件
Fragment
Teleport
Suspense
- 其他改变
新的生命周期钩子
data 选项应始终被声明为一个函数
移除keyCode支持作为 v-on 的修饰符
…
二、创建Vue3工程
1.使用 vue-cli 创建
官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue_test
## 启动
cd vue_test
npm run serve
2.使用 vite 创建
官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite
vite官网:https://vitejs.cn
什么是vite?—— 新一代前端构建工具。
优势如下:
开发环境中,无需打包操作,可快速的冷启动。
轻量快速的热重载(HMR)。
真正的按需编译,不再等待整个应用编译完成。
传统构建 与 vite构建对比图
注:他们创建的时间都是差不多的,只是在运行启动时快慢不同!
(1)传统构建

传统构建模式,是将所有资源都打包好,再上线
(2)vite构建

而Vite是按需加载
接下来我们就用Vite来创建一个Vue3的项目
## 创建工程,前面三个是不可变的,最后一个是工程的名字
npm init vite-app <project-name>
# same as :npm会自动转换为如下命令执行:
# npx create-vite-app <project-name>
## 进入工程的目录
cd <project-name>
## 安装依赖!!!这个是特殊之处,不会自动安装的
npm install # npm i也可以
## 运行
npm run dev
3.关闭eslint语法检查
- 根目录新建vue.config.js
module.exports={
lintOnSave:false //关闭语法检查
}
这样有一些错误就不会在web页面报错,只是效果出不来而已(但是开发时一般不用关)
4.分析文件目录
4.1 main.js
Vue2项目的main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
我们再来看看Vue3项目中的main.js
特点:更加的简洁,用工厂函数创建app对象,而不是直接使用new Vue()的方式,把vue2的逻辑封装了起来更加方便
并且这里创建出来的app对象不完全是vm对象了,app类似于之前Vue2中的vm,但app比vm更“轻”,即没有那么多冗余的属性和方法
并且vue3不需要这句话:Vue.config.productionTip = false
这句话的意思:阻止显示处于生产模式的消息(因为这个消息没什么意义,我们开发的时候肯定是处于生产模式,如果出现会显得比较冗余杂乱)
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
分析一下
// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”,即没有那么多冗余的属性和方法)
const app = createApp(App)
// console.log(app)
// 挂载:(这样页面上才会出东西)
app.mount('#app')
// 卸载:(这样页面上就没东西了)
// setTimeout(() => {
// app.unmount("#app"); //用unmount函数卸载
// },2000)
注意
我们在vue3工程中,书写main.js时如果再使用原来vue2那套是行不通的!
4.2 .vue组件
我们再来看看组件
在template
标签里可以没有根标签了(但是也可以有)
以App.vue为例:
<template>
<!-- Vue3组件中的模板结构可以没有根标签,即可以没有<div id="xxx"></div>-->
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
5.Vue3.0中的响应式原理
5.1 vue2.x的响应式
底层:
let person = {
name: '张三',
age: 18
};
//模拟vue2中的响应式
//创建一个空对象p
let p = {};
//给p对象身上追加属性name(在原型上加属性和方法)
Object.defineProperty(p, 'name', {
get(){
//get name
return person.name;
},
set(value){
//set name
console.log('name changed, must change the page'); //这里其实有深层的逻辑,说白了就是发现数据被修改了,要更新页面!
person.name = value;
}
})
//给p对象身上追加属性age
Object.defineProperty(p, 'age', {
configurable: true,
get(){
//get age
return person.age;
},
set(value){
//set age
console.log('age changed, must change the page');
person.age = value;
}
})
总结:
实现原理:
对象类型:通过
Object.defineProperty()
对属性的读取、修改进行拦截(==数据劫持==)。数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
jsObject.defineProperty(对象, '属性', { get () {}, set () {} })
存在问题:
只能拦截到读取和修改属性,所以后面再新增和删除属性, 界面不会更新。——>最大的缺点!
(1)新增:
需要使用this.$set(this.对象, ‘属性’, ‘属性值’)或者 Vue.set(this.对象, ‘属性’, ‘属性值’)来进行新增
(2)删除:
需要使用this.$delete(this.对象, ‘属性’)或者 Vue.set(this.对象, ‘属性’)来进行删除
直接通过下标修改数组, 界面不会自动更新。
(1)修改:
使用this.$set(this.数组, 索引, ‘值’)或者 Vue.set(this.数组, 索引, ‘值’)来进行修改
(2)修改、新增或删除:
使用this.数组.splice(索引, 个数, ‘值’)
新增就把个数写为0
删除就不用指定值
5.2 Vue3.0的响应式
注:这里说的是Proxy对象的方式实现响应式,Vue3中的ref函数操作基本类型属性依然用的是vue2中的get,set方法!
底层:
let person = {
name: '张三',
age: 18
};
//模拟vue3中的响应式
//Proxy代理:让p对象去映射对于person的操作,其实和Object.defineProperty方法是类似的!
const p = new Proxy(person, {
//捕获数据的修改:所有的属性共用一套get,set!非常轻便!
//读取
get(target, propName) {
//person对象 ==> target
//person对象的属性 ==> propName
console.log(`有人读取了p身上的${propName}属性`);
//return target[propName] //直接改,和下面效果等价
return Reflect.get(target, propName); //这个get和我们定义的get不是一个概念!
},
//修改/增加
set(target, propName, value) {
// 修改和增加属性都调用set
console.log('修改,更新页面');
Reflect.set(target, propName, value);
},
//删除
deleteProperty(target, propName) {
console.log(`删除,更新页面`);
return Reflect.deleteProperty(target, propName);
}
});
Reflect原理:
Reflect.defineProperty相比Object.defineProperty的优势:会有返回值,可以判断执行失败还是成功了,不需要捕获异常,更加健壮!
let obj = {
a:1,
b:2
}
const x1 = Reflect.defineProperty(obj, 'c',{
get(){
return 3;
}
});
console.log(x1); //true 成功
const x2 = Reflect.defineProperty(obj, 'c',{ //这里如果是Object.defineProperty就会报错,但是这里会返回false!这是有优势的!
get(){
return 4;
}
});
if(x2){ //可以进行判断捕获
console.log(x2); //false 否
}
Reflect.get(obj,'a');
总结:
实现原理:
通过Proxy(代理对象): 拦截对象中任意属性的任意变化, 包括:属性值的读写、属性的添加、属性的删除等。
所以说不存在vue2中的添加、删除属性不能实现响应式的问题!
不需要$set了!
通过Reflect(反射对象): 对源对象的属性进行操作,并且有返回值表示执行的状态。
MDN文档中描述的Proxy与Reflect:
Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
jsnew 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'
这样就实现了利用对Proxy代理对象进行更新,拦截了更新的动作,然后利用Reflect反射对象对原来的
数据进行更新,从而实现页面绑定的数据的更新,从而实现响应式!
三、常用 Composition API
Composition API:组合式API
官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html
1.拉开序幕的setup
<template>
<!--vue3的组件模版结构可以没有根标签-->
<h1>我是app组件</h1>
<h1>我叫{{ name }}, {{ age }}岁</h1>
<button @click="sayHello">hello!!</button>
<h3>msg: {{ vue2 }}</h3>
<p>数据冲突该怎么办?{{ a }}</p>
<button @click="test1">测试一下在vue2中去读取vue3的配置</button>
<button @click="test2">测试一下在vue3的setup中去读取vue2的配置</button>
</template>
<script>
// import { h } from 'vue';
export default {
name: "App",
//测试使用vue2的内容
data() {
return {
vue2: "still can use vue2 in vue3 code",
a: 1,
};
},
methods: {
//vue2配置方法的方式
test1() {
console.log(this.vue2);
console.log(this.name);
console.log(this.sayHello);
this.sayHello();
},
},
//此处只是测试setup,暂时先不考虑响应式的问题
//响应式:就是数据绑定,数据更改了就进行相应的改变
setup() {
//表演的舞台
//准备数据 data
let name = "py";
let age = 21;
let a = 300;
//方法 methods,读取vue3的配置:可以读取!
function sayHello() {
alert(`My name is ${name}, ${age} ${age === 1 ? "year" : "years"} old`);
}
//在vue3的配置里去读取vue2配置的属性:读取不到!
function test2() {
console.log(name);
console.log(age);
console.log(sayHello);
console.log(this.vue2);
console.log(this.test1);
}
//选择1:返回一个对象
return {
name,
age,
sayHello,
test2,
a,
};
//选择2:返回一个渲染函数
//这是直接将你在这里渲染的东西替换到template中
//第一个参数:要把哪个元素放到页面 //第二个参数:元素里面的内容
// return () => h('h1', 'hello');
//这样模板template里面写什么都没有用了,就以渲染函数为主
//所以页面上只会出现大大的hello
},
};
</script>
总结:
- 理解:Vue3.0中一个新的配置项,值为一个函数。
- setup是所有Composition API(组合API)“ 表演的舞台 ”,也就是说主要是用来写组合式API的!
- 组件中所用到的:数据、方法等,均要配置在setup中(也就是说不用写data和methods等配置项了),当然组合式API也要写在setup中!
- setup函数的两种返回值:
若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
若返回一个渲染函数:则可以自定义渲染内容。(了解)
- 注意点:
==setup尽量不要与Vue2.x配置混用==
- Vue2.x的配置项依然可以用
- Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
- 但==在setup中不能访问到Vue2.x配置(data、methos、computed…)中的属性、方法==。
- 如果有重名的, 以setup内的为主。
==setup不能是一个async函数==,因为返回值不再是return原本的对象, 而是被promise包裹的对象, 模板就看不到return的对象中的属性了(因为原本的对象被包裹住了)。
注:后期也可以返回一个Promise实例,但需要Suspense组件和defineAsyncComponent异步引入的配合使用才行,所以这样setup也可以是async函数了,同时还可以配合await使用!
2.setup的两个注意点
注:vue2中的两个补充点
(1)Vue2中的this.$attrs:这个就是用来兜底的,在props属性中没有进行声明的,但是外部传进来的,就会出现在这个attrs属性中
(2)Vue2中的this.$solts:这个是用来存插槽接收到的内容,如果没有在子组件中写<solt></solt>
,那么内容不会在页面上显示,但是父组件的子组件标签内的内容(以虚拟dom的形式)会存储到这个solts属性中,并不会丢失(不管声明不声明<solt></solt>
都会出现在solts中)
setup的两个注意点
Demo.vue
<template>
<!--vue3的组件模版结构可以没有根标签-->
<h1>我是app组件</h1>
<h1>我叫{{ person.name }}, {{ person.age }}岁</h1>
<button @click="test">测试触发一次demo的自定义事件</button>
</template>
<script>
import { reactive } from 'vue';
export default {
name: 'Demo',
beforeCreate() {
console.log('----@bc');
},
props: ['msg', 'school'], //还是一样要声明
emits:['hello'], //要声明触发的事件
setup(props, context){
// console.log('----setup');
// console.log(this); ///undefined
console.log(props); //props: 外部给组件丢的参数 => 响应式(Proxy实例)
let person = reactive({
name: 'py',
age: 21,
});
// console.log(context, context.attrs); 相当于vue2中的$attrs
// console.log(context,context.slots); 插槽
//返回一个对象
return {
person,
test(){
context.emit('hello', 666); //触发自定义事件
}
}
}
}
</script>
App.vue
<template>
<!--vue3的组件模版结构可以没有根标签-->
<Demo @hello="showHelloMsg" msg="hello" school="wust">
<!--注意:现在slot="qwe"这种写法弃用了,在vue3中不要这么写!-->
<template v-slot:qwe>
没事
</template>
<template v-slot:asd>
love you
</template>
</Demo>
</template>
<script>
import Demo from "./components/Demo";
export default {
name: 'App',
components: {Demo},
setup(){
return {
showHelloMsg(value){
alert(`你好,${value}`);
}
}
}
}
</script>
总结:
1.setup执行的时机
- 在beforeCreate(最先执行的钩子)之前执行一次,并且this是undefined(因为vm还没创建出来)。
- 所以说setup函数里面不需要使用this,因为根本没法使用,所以说setup里面所有函数都可以用箭头函数!
2.setup的参数
(1)props:父组件传递的数据响应式集合对象,依然需要props:[]接收,才能在setup函数内打印出props参数
注意:但是这里props声明的和传入的必须一样,如果有出入会报警告,并且这次的有出入的参数会存到context对象的attrs对象中;和vue2不一样,vue2不会有警告,并且是存在vm的$attrs内
其余的context对象的slots、emit对象同理!
(2)context:上下文对象
1)context.attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于vue2的 this.$attrs。
2)context.slots: 收到的插槽内容,相当于vue2的 this.$slots。
注意:现在slot="name"这种写法弃用了,统一使用v-slot:name这样的写法!
还有简写的方式,可以写#name,就相当于v-slot:name
3)context.emit: 分发(触发)自定义事件的函数, 相当于vue2的 this.$emit。
在vue2中子组件触发事件并向父组件传值,this.$emit(‘test’,1)
在vue3中第一步要定义**emits:[“test”]**声明将要触发父组件的事件名,第二步在合适的时机进行触发事件 context.emit(‘test’,1)
3.ref函数
我们发现如果还是用去取setup中的数据,那数据改变页面不会变了!
原因:直接在setup里面定义的数据根本就不是响应式数据,vue检测不到!
ref函数应用场景:针对setup中的非响应式数据,即数据会变但页面不会变的情况
<template>
<!--vue3的组件模版结构可以没有根标签-->
<h1>我是app组件</h1>
<h1>我叫{{ name }}, {{ age }}岁</h1>
<h3>职位:{{ job.type }}</h3>
<h3>薪水:{{ job.salary }}</h3>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
//引用ref函数
import { ref } from 'vue';
export default {
name: 'App',
setup(){
//表演的舞台(setup)
//准备数据 data
//ref实现响应式(基本类型):也是采用Object.definedProperty()来实现的,即getter和setter方法
let name = ref('py'); //创建ref引用对象:把数据交给ref函数加工一下作为一个变量!
let age = ref(21);
//ref实现响应式(对象类型):reactive函数实现,本质上是采用ES6中的Proxy对象来实现的
let job = ref({
type: 'frontend developer',
salary: '30'
});
function changeInfo(){
name.value = '李四'; //要用.value来进行修改,否则依然不行,因为ref对象里面的value才是我们想要的属性!
age.value = 42;
job.value.type = 'UI developer'; //取对象里面的属性就不需要再次.value了!
console.log(name, age); //不是响应式的
}
//返回一个对象
return {
name,
age,
job,
changeInfo
}
}
}
</script>
总结:
解释:这里的ref是一个函数,与vue2中的不同,vue2中的ref只是标签的一个属性,作用类似于id,而这里不同!
作用: 定义一个响应式的数据
语法: const xxx = ref(initValue)
创建一个包含响应式数据的引用对象(RefImpl对象,又称reference对象,全称为‘引用实现的实例对象’,简称ref对象),ref对象.value就是我们想要的数据
在JS中操作数据: xxx.value
模板中读取数据: 不需要 变量名.value,直接:
<div></div>
备注:
接收的数据可以是:基本类型、也可以是对象类型。
基本类型的数据:响应式依然是靠Object.defineProperty()的getter与setter完成的(数据劫持)。
对象类型的数据:对象内部的属性的响应式 “ 求助 ” 了Vue3.0中的一个新函数 —— reactive函数,本质上是采用ES6中的Proxy对象来实现的。
4.reactive函数
<template>
<!--vue3的组件模版结构可以没有根标签-->
<h1>我是app组件</h1>
<h1>我叫{{ person.name }}, {{ person.age }}岁</h1>
<h3>职位:{{ person.type }}</h3>
<h3>薪水:{{ person.salary }}</h3>
<h3>爱好:{{ person.hobbies }}</h3>
<h4>测试的数据c:{{ person.a.b.c }}</h4>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import { reactive } from 'vue';
export default {
name: 'App',
setup(){
//表演的舞台(setup)
//准备数据 data
//ref实现响应式(基本类型)也是采用Object.definedProperty()来实现的 getter和setter
// let name = ref('py'); //ref引用对象(RefImpl对象)实例
// let age = ref(21);
//ref实现响应式(对象类型)也是采用Proxy来实现(proxy) 这里如果就算是用ref也是借助了reactive
let person = reactive({
name: 'py',
age: 21,
type: 'frontend developer',
salary: '30',
hobbies: ['抽烟', '喝酒', '烫头'],
a:{
b:{
c: 666
}
}
});
//注:注意这里即便是得到对象也不需要.value了,得到对象的属性就更加不需要.value了!
function changeInfo(){
//基本类型不能响应式,因为reactive处理不了,套路:我们把基本类型作为属性封装为对象!
person.name = '李四';
person.age = 42;
//原来的:job.value.type = 'xxx'
person.type = 'UI developer';
//测试reactive能否监测深层次变化:可以监测!
person.a.b.c = 100;
//测试reactive能否处理数组:可以!
person.hobbies[0] = 'play tennis';
}
//返回一个对象
return {
person,
changeInfo
}
}
}
</script>
总结:
作用: 定义一个对象类型的响应式数据(==基本类型不要用它,要用ref函数==)
语法:
const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称Proxy对象)
reactive定义的响应式数据是“深层次的”,对象的属性也会成为响应式的。
内部基于 ES6 的 Proxy 实现,Proxy是响应式的,由Object——>Proxy,通过代理对象操作源对象内部数据进行操作(数据劫持)。
一般的定义方式:
let data = reactive({
person:{},
student:{}
});
5.reactive对比ref
从定义数据角度对比:
ref用来定义:基本类型数据。
reactive用来定义:对象(或数组)类型数据。
备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象。
从原理角度对比:
ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)。
reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
从使用角度对比:
ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value。
reactive定义的数据:操作数据与读取数据,均不需要.value。——> 用的更多一点
6.计算属性与监视
6.1 computed函数
<template>
<h1>一个人的信息</h1>
姓:<input type="text" v-model="person.firstName"/>
<br/>
名:<input type="text" v-model="person.lastName"/>
<p>姓名:{{ person.fullName }}</p>
<br/>
修改全名:<input type="text" v-model="person.fullName"/>
</template>
<script>
import { reactive, computed } from 'vue'; //引入计算属性
export default {
name: 'Demo',
//vue2的写法:
// computed:{
// fullName(){
// //vue2看得到vue3,要注意
// return this.person.firstName + '-' + this.person.lastName;
// }
// },
//vue3的写法:
setup(){
let person = reactive({
firstName: 'pan',
lastName: 'yue',
age: 21,
});
//计算属性(简写,没有考虑计算属性被修改的情况)
// 注1:可以直接给person添加一个fullName计算属性!保证是响应式的
// 注2:可以使用箭头函数,因为setup函数里面不需要使用this,因为setup的this是undefined的!!!
// person.fullName = computed(() => {
// return person.firstName + '-' + person.lastName;
// })
//计算属性(完整写法,既考虑了读也考虑了改)
person.fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
//修改
set(name){
//解构赋值,这里也可以写let a = ...,然后下面用a[0]、a[1]去接收
let [ fn, ln ] = name.split('-');
//响应式
person.firstName = fn;
person.lastName = ln;
}
})
//返回一个对象
return {
person,
}
}
}
</script>
总结:
与Vue2.x中computed配置功能一致
写法
jsimport {reactive,computed} from 'vue' setup(){ let person = reactive({ firstName:'', lastName:'' }) //计算属性——简写 person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) //计算属性——完整 person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] } }) }
6.2 watch函数
<template>
<h1>当前求和为:{{ sum }}</h1>
<button @click="sum++">点我加一</button>
<hr/>
<h2>当前的信息为:{{ msg }}</h2>
<button @click="msg += '!'">修改信息</button> <!--每次多加一个叹号!-->
<hr/>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>薪资:{{ person.job.j1.salary }}K</h2>
<button @click="person.name = person.name + '~'">修改姓名</button>
<button @click="person.age++">增长年龄</button>
<button @click="person.job.j1.salary++">增长薪资</button>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
name: 'Demo',
//vue2中的watch写法:
// watch:{
// //简单写法
// // sum(nv,ov){
// // console.log('sum的值发生变化了');
// // console.log(`newValue:${nv}, oldValue:${ov}`);
// // }
//
// //完整写法
// sum:{
// deep: true, //深度监视
// immediate: true, //一开始就监视一下
// handler(nv,ov){
// console.log('sum的值发生变化了');
// console.log(`newValue:${nv}, oldValue:${ov}`);
// }
// }
// },
//vue3中的watch写法:
setup(){
let sum = ref(0);
let msg = ref('你好');
let person = reactive({
name: '张三',
age: 18,
job:{
j1:{
salary: 20
}
}
})
let person2 = ref({
name: '张三',
age: 18,
job:{
j1:{
salary: 20
}
}
})
//情况一: 监视ref所定义的响应式数据
// 注意:这里不用.value,否则报错,因为存的是基本类型数据,不能直接监视数据值,而应该监视一个变量或者对象!
// watch(sum, (nv, ov) => {
// //这里我并不需要this,所以箭头函数、普通函数都可以
// console.log('sum的值发生变化了');
// console.log(`newValue:${nv}, oldValue:${ov}`);
// }, {
// //监视的配置
// immediate: true //一上来就更新
// });
//注意:如何监视ref定义的对象?
// 方式一:这里需要.value,否则无效,因为属性在ref对象的person2对象的里面,属性改变不会造成person2的地址值改变,所以监测不到改变。如果.value就得到了一个Proxy对象,默认deep的对象
// watch(person2.value, (nv, ov) => {
// console.log(`newValue:${nv}, oldValue:${ov}`);
// });
// 方式二:这里不需要.value,开启deep即可,这样person2里面的属性变化就可以监视到了
// watch(person2, (nv, ov) => {
// console.log(`newValue:${nv}, oldValue:${ov}`);
// }, {
// deep: true
// });
//情况二: 监视ref所定义的多个响应式数据
// 注意:这里不用.value,否则报错
// watch([sum, msg], (nv, ov) => {
// //此时nv和ov都是被监视属性值的数组
// // console.log(Array.isArray(ov)); //true
// console.log('sum的值或者msg的值发生变化了');
// console.log(`newValue:${nv}, oldValue:${ov}`);
// },{
// immediate: true
// });
/**
* 情况三:监视reactive所定义的一个响应式数据(ref定义的对象也一样,因为底层还是reactive)
* reactive定义的数据在进行监视时的两个坑:
* 1.此处无法获取正确的ov(oldValue) ——> 目前还没有解决的bug,但是oldValue本来就不重要
* 2.强制开启了深度监视
*/
// watch(person, (nv, ov) => {
// console.log('person变化了');
// console.log(nv, ov);
// }, {
// deep: false //此处的deep配置是无效的,会强制为true
// });
//情况四:监视reactive所定义的响应式数据中的某一个属性
// 属性要写成函数,作为其返回值,() => person.age
// watch(() => person.age, (nv, ov) => {
// console.log('person的age变了', nv, ov);
// })
//情况五:监视reactive所定义的响应式中的多个属性
// watch([() => person.age, () => person.name], (nv, ov) => {
// //此时nv和ov都是数组
// console.log('person的age或name发生改变了',nv, ov);
// });
//特殊情况:监视reactive所定义的对象里面的对象
// watch(() => person.job, (nv, ov) => {
// //这里依然无法拿到正确的ov,因为依然监视的是对象
// console.log('person的job信息发生改变了',nv, ov);
// }, {
// //这里必须要加deep:true 注意!
// deep: true //此处因为监视的是reactive所定义的响应式对象的一个属性(这个属性的值它依然是一个对象),所以deep配置就有效了,且默认为false
// })
//返回一个对象
return {
sum,
msg,
person
}
}
}
</script>
总结
- 与Vue2.x中watch配置、功能一致
监视reactive定义的响应式数据时,三个小“坑”:
oldValue无法正确获取
默认强制开启了深度监视(deep:false关闭配置失效)。
监视reactive定义的响应式数据中某个属性时:deep配置有效。
6.3 watchEffect函数
<template>
<h1>当前求和为:{{ sum }}</h1>
<button @click="sum++">点我加一</button>
<hr/>
<h2>当前的信息为:{{ msg }}</h2>
<button @click="msg += '!'">修改信息</button>
<hr/>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>薪资:{{ person.job.j1.salary }}K</h2>
<button @click="person.name = person.name + '~'">修改姓名</button>
<button @click="person.age++">增长年龄</button>
<button @click="person.job.j1.salary++">增长薪资</button>
</template>
<script>
import {reactive, ref, watch, 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
}
}
});
//监测的不是一个值,而是一个保存值的结构(不能写成sum.value) ,不能监视一个具体的值 注意
watch(sum, (nv, ov) => {
console.log(nv, ov);
},{
immediate: true
});
//watchEffect函数
//特点:不确定监视对象,用谁就监视谁,并且默认开启了immediate:true
//好处:实现了业务逻辑的复用,也就是说多个属性可以共用一套监视逻辑!
watchEffect(() => {
console.log(`watch effect指定的回调执行了!!!`)
//依赖收集,你用到了谁它就监视谁!!
//这里用到sum,person.job.j1.salary了,所以可以被监视到(只要它们发生变化就重新执行watchEffect,重新进行一系列的逻辑)
//与computed有点类似,依赖收集(即回调函数里面依赖的数据发生变化就重新运行)。但是侧重点不一致,即它们的回调函数的内容侧重不同,watchEffect注重过程的执行(业务逻辑的运行),而computed注重计算函数的返回值(属性的动态计算)
const x1 = sum.value;
const x2 = person.job.j1.salary;
//不需要return
})
//返回一个对象
return {
sum,
msg,
person
}
}
}
</script>
总结:
watch的套路是:既要指明监视的属性,也要指明监视的回调。
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。只要有变化就会执行回调
watchEffect有点像computed:
但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
js//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 watchEffect(()=>{ const x1 = sum.value const x2 = person.age console.log('watchEffect配置的回调执行了') })
7.生命周期onMounted()
vue2.x的生命周期
vue3.0的生命周期
Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:销毁变成了卸载
即Vue2中的钩子函数在Vue3中依然适用!
- beforeDestroy改名为 beforeUnmount
- destroyed改名为 unmounted
控制组件的挂载和卸载=>使用v-if
Vue3.0也提供了 Composition API 形式的生命周期钩子(写在setup函数里面的),与Vue2.x中钩子对应关系如下:只变名,不变功能
- beforeCreate ==>setup()本身
- created ==>setup()本身
- beforeMount ==>onBeforeMount
- mounted ==>onMounted
- beforeUpdate ==>onBeforeUpdate
- updated ==>onUpdated
- beforeUnmount ==>onBeforeUnmount
- unmounted ==>onUnmounted
- 注:对应的两者最好不要同时出现,当同时出现时,带on的要先于不带的!即组合式Api的优先级更高一点!
实战:
<template>
<h1>当前求和为:{{ sum }}</h1>
<button @click="sum++">点我加一</button>
<hr/>
</template>
<script>
//组合式API的理解:可以import引入很多函数,然后组合到setup函数中去!
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';
export default {
name: 'Demo',
setup(){
let sum = ref(0);
//1.通过 组合式api 的形式去使用生命周期钩子 ——> 在setup里面写!
//setup()相当于beforeCreate()和created()
onBeforeMount(() => { console.log('----beforeMount----'); });
onMounted(() => { console.log('-----mounted-----'); });
onBeforeUpdate(() => { console.log('-----beforeUpdate-----') });
onUpdated(() => { console.log('-----updated-----'); });
onBeforeUnmount(() => { console.log('-----beforeUnmount----'); });
onUnmounted(() => { console.log('-----unmounted----'); })
console.log('-----setup----')
//返回一个对象
return {
sum,
}
},
//2.使用 配置项 的形式使用生命周期钩子 ——> 在setup外面写!
// beforeCreate() {
// console.log('----beforeCreate!!----');
// },
// created() {
// console.log('----created!!----');
// },
// beforeMount() {
// console.log('----beforeMount----');
// },
// mounted() {
// console.log('-----mounted-----');
// },
// beforeUpdate() {
// console.log('-----beforeUpdate-----')
// },
// updated() {
// console.log('-----updated-----');
// },
// beforeUnmount() {
// console.log('-----beforeUnmount----');
// },
// unmounted() {
// console.log('-----unmounted----');
// }
}
Vue2的钩子函数和Vue3中的Composition API 形式的生命周期钩子的关系图示:
8.自定义hook函数
其实就相当于工具类的感觉
../hooks/usePoint.js
数据,方法,生命周期钩子
hook文件的命名一般都是:useXxx
//得到鼠标点坐标的api
import { reactive, onMounted, onBeforeUnmount } from "vue";
//export default function usePoint(){
export default function(){ //简写:可以没有名字,叫什么名字是根据引入的人来决定的
//1.响应式数据
let point = reactive({
x: 0,
y: 0
});
//2.方法
const savePoint = event => {
console.log(event.pageX, event.pageY);
point.x = event.pageX;
point.y = event.pageY;
};
//3.生命周期
//挂载时就绑定事件监听
onMounted(() => {
window.addEventListener('click', savePoint)
});
//卸载之前移除事件监听
onBeforeUnmount(() => {
//在卸载之前取消事件的监听,因为组件如果不用了,事件也不应该监听了
window.removeEventListener('click', savePoint);
});
return point;
}
Demo.vue
<template>
<h1>当前求和为:{{ sum }}</h1>
<button @click="sum++">点我加一</button>
<hr/>
<h2>当前点击时鼠标的坐标为x:{{ point.x }}, y:{{ point.y }}</h2>
</template>
<script>
import {ref} from 'vue';
import usePoint from "../hooks/usePoint";
export default {
name: 'Demo',
setup(){
let sum = ref(0);
//复用自定义hooks,调用hook函数,得到数据、方法、生命周期钩子
const point = usePoint();
//返回一个对象
return {
sum,
point
}
},
}
</script>
Test.vue
<template>
<h2>我是test组件</h2>
<h3>{{ point }}</h3>
</template>
<script>
import usePoint from "../hooks/usePoint";
export default {
name: "Test",
setup(){
const point = usePoint(); //复用打点hook(封装数据以及其要用的生命周期钩子),这也是组合式api的好处
return {
point
}
}
}
</script>
总结
什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,里面有响应式数据和一系列的逻辑。
所以Composition API,即组合式API的理解:包括ref、reactive、computed、watch、生命周期函数等,里面有数据和逻辑
类似于vue2.x中的mixin。
自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
操作步骤
- 在src文件夹下创建hooks文件夹
- 在hooks文件夹下创建usePoint.js文件(常以use开头)
- 定义方法并return出去point,然后再其他组件中既可以调用使用return的值
9.toRef和toRefs
Demo.vue
目的:实现模板中可以直接引用属性,不需要 对象.属性
<template>
<h4>{{ person }}</h4>
<h2>姓名:{{ name }}</h2> <!--之前是person.name-->
<h2>年龄:{{ age }}</h2>
<h2>薪资:{{ salary }}K</h2>
<button @click="name = name + '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="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
}
}
});
const name1 = person.name; //普通的赋值操作
console.log('@@@@@', name1); //注意 这里输出的是字符串,并不是响应式的数据
const name2 = toRef(person,name); //返回的是RefImpl对象,这里的name2与person.name是完全一模一样的(你改这里的name2与你改上面的person.name是一码事),且数据还是响应式的
console.log('####', name2);
const x = toRefs(person);
console.log(x);
//toRef就是返回一个RefImpl对象(toRef是引用 name就是person.name且为响应式)
//toRef处理一个,而toRefs处理一群
//大白话:toRef(s)就是方便我们把响应式数据(ref或者reactive生成的)展开丢出去,方便在模版中直接应用(就不用对象.属性了,直接写属性即可)
return {
person,
//普通写法:toRef
// name: toRef(person, "name"),
// age: toRef(person, "age"),
// salary: toRef(person.job.j1, "salary")
//简便写法:toRefs
...toRefs(person), //将所有属性转换为ref对象,包裹成为一个大对象,ES6的...进行对象的拆解
//注意:但是toRefs只会转换第一层的属性,我们拿不到深层的属性,所以还需要单独使用toRef
salary: toRef(person.job.j1, 'salary') //toRef可以与toRefs连用,更加方便,可以取到深层的属性!
};
//注意千万不能这样写!
//一旦这样写就与元数据分离了,虽然页面数据会变,但是改name不会引起原来的person.name的变化(因为ref把name值包装成了一个新的refImpl对象,并没有指向原来的person的属性,与原来的person就没有关系了!是两个不同的数据!)
// return {
// person,
// name: ref(person.name),
// age: ref(person.age),
// salary: ref(person.job.j1.salary)
// };
//这样写就更不行了,因为person.name就相当于一个普通的字符串,一个普通的变量,与元数据没有任何关系
// return {
// person,
// name: person.name,
// age: person.age,
// salary: person.job.j1.salary
// };
}
}
</script>
总结:
- 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
- 语法:const name = toRef(person,'name'),html中就可以直接使用name。
- 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
- 扩展:toRefs与toRef功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person),相当于一个对象
创建的新的ref对象指向的还是原来的那个属性:所以说toRef就相当于一个桥接的作用!
toRef写法:
toRefs写法:
四、其它 Composition API
1.shallowReactive 与 shallowRef
<template>
<h2>当前的y是:{{ x.y }}</h2>
<button @click="x = {y : 888}">点我替换x</button>
<button @click="x.y++">点我y+1</button>
<hr/>
<h4>{{ person }}</h4>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>薪资:{{ job.j1.salary }}K</h2>
<button @click="name = name + '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">增长薪资</button>
</template>
<script>
import {ref,reactive, toRefs, shallowReactive, shallowRef} from 'vue';
export default {
name: 'Demo',
setup(){
// shallowReactive只考虑对象类型的第一层数据响应式:浅层次的reactive
// 所以这里salary就不是响应式的了
// let person = shallowReactive({
// name: '张三',
// age: 18,
// job:{
// j1:{
// salary: 20
// }
// }
// });
let person = reactive({
name: '张三',
age: 18,
job:{
j1:{
salary: 20
}
}
});
// let x = ref(0);
//对于基本类型来说,ref与shallowRef基本是没什么区别的
// let x = shallowRef(0);
//但注意对于对象类型来说,shallowRef只能处理对象类型的第一层,而ref可以处理对象类型全部层次的响应式
//但注意不管是shallowRef还是非shallow, 第一层都是响应式的(比如下面的x是响应式的数据),只不过shallowRef的对象里面的属性不是响应式的
//所以x是可以进行替换的,但是里面的属性不能修改!
let x = shallowRef({ y: 0 });
console.log(x);
// let x = ref({ y: 0 });
// console.log(x);
return {
person,
...toRefs(person),
x,
};
}
}
</script>
总结:
shallowReactive:只处理对象最外层属性的响应式(浅响应式),如果对象第一层属性也是对象就不会响应式。
shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理(只能处理对象类型的第一层)。
什么时候使用?
如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
如果有一个对象数据,后续功能不会修改该对象中的属性(常量对象),而是生成新的对象来替换 ===> shallowRef。
2.readonly 与 shallowReadonly
<template>
<h2>当前求和为:{{ sum }}</h2>
<button @click="sum++">sum+1</button>
<hr/>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>薪资:{{ job.j1.salary }}K</h2>
<button @click="name = name + '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">增长薪资</button>
</template>
<script>
import {ref,reactive, toRefs, readonly, shallowReadonly} from 'vue';
export default {
name: 'Demo',
setup(){
let person = reactive({
name: '张三',
age: 18,
job:{
j1:{
salary: 20
}
}
});
// person = readonly(person); //此时person里面的属性值都不允许修改
// person = shallowReadonly(person); //第一层不能改(name,age), 但j1和salary仍然可以改动
let sum = ref(0);
// sum = readonly(sum); //同理
// sum = shallowReadonly(sum) //对于基本类型来说readonly和shallowReadonly没区别
return {
sum,
...toRefs(person),
};
}
}
</script>
总结:
readonly: 让一个响应式数据变为只读的(深只读)。person = readonly( person )
shallowReadonly:让一个响应式数据变为只读的(浅只读),对象里的第一层不能改,但深层的数据可以改。
应用场景: 当不希望数据被修改时,保证安全。
因为响应式的数据可能不是我们自己定义的,是别人定义的,我们不想修改。
或者有的用户无权限进行修改,而有的有权限。
3.toRaw 与 markRaw
<template>
<h2>当前求和为:{{ sum }}</h2>
<button @click="sum++">sum+1</button>
<hr/>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>薪资:{{ job.j1.salary }}K</h2>
<h3 v-show="person.car">座驾信息:{{ person.car }}</h3>
<button @click="name = name + '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">增长薪资</button>
<button @click="showRawPerson">输出最原始的person</button>
<button @click="addCar">给人添加一台车</button>
<button @click="person.car && (person.car.name +='!') ">换车名</button>
<button @click="changePrice">换价格</button>
</template>
<script>
import {markRaw, reactive, ref, toRaw, toRefs} from 'vue';
export default {
name: 'Demo',
setup(){
let sum = ref(0);
let person = reactive({
name: '张三',
age: 18,
job:{
j1:{
salary: 20
}
}
});
//toRaw用于消除数据的响应式
const showRawPerson = () => {
const p = toRaw(person);
// console.log(person); //proxy代理对象 Proxy {....}
// console.log(p); //原始对象数据 {....}
p.age++; //注意此时页面不会再发生变化了,只是普普通通的对象,不是响应式
// const sum = toRaw(sum);
// console.log(sum); //undefined //这条路走不通,toRaw只能处理reactive生成的Proxy对象
}
//markRaw用于剥夺数据的响应式权利
const addCar = () => {
person.car = markRaw({
name: 'benz',
price: 40
}); //在响应式的对象身上添加任何属性都是响应式的,经过markRaw一包装就变成最原始的数据,就不会再做响应
}
const changePrice = () => {
//?.是js语法中的可选链,只有当car存在时才会去访问price属性!
//下面的意思就是:只有当car存在时,才会把price进行++,也就是点击那个按钮才有用
person.car?.price && person.car.price++;
console.log(person?.car?.price);
}
return {
sum,
person,
...toRefs(person),
showRawPerson,
addCar,
changePrice
};
}
}
</script>
总结:
toRaw:
作用:将一个由reactive生成的响应式对象转为普通对象,就是普通的person对象。
使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
markRaw:
作用:标记一个对象,使其永远不会再成为响应式对象。
应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类库等(有时需要把工具库放在对象身上)。
- 当渲染具有不可变数据源的大列表时(没有进行修改的需求),跳过响应式转换可以提高性能。——> 性能极致化
4.customRef(自定义ref)
<template>
<input v-model="keyWord"/>
<h3>{{ keyWord }}</h3>
</template>
<script>
import { ref, customRef } from 'vue';
export default {
name: 'App',
setup(){
// let keyWord = ref('hello'); //使用vue提供的内置ref,
let keyWord = myRef('hello', 500); //使用自定义ref
//自定义ref(customRef):手动挡,可玩性更大
function myRef(value, delay){
return customRef((track, trigger) => {
let timer;
return {
get(){
console.log(`从myRef这个容器读取数据,data:${value}`);
track(); //track通知vue追踪一下value的变化(跟getter商量一下让它明确你这个新的value是有用的)
return value; //读取的时候就会调用get
},
set(newValue){
console.log(`myRef容器中的数据被修改,data改为${newValue}`);
clearTimeout(timer); //清楚之前的定时器,重新开启,防抖
timer = setTimeout(() => { //定时器,延时修改
value = newValue; //把修改后的值赋值给传入的value
trigger(); //trigger通知vue重新解析模版 //防抖,只有在定时器结束,最终运行到trigger时才会重新解析模板
},delay);
}
}
});
}
return {
keyWord,
}
}
}
</script>
修改input的数据时触发的逻辑:
总结:
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
实现防抖效果:
vue<template> <input type="text" v-model="keyword"> <h3>{{keyword}}</h3> </template> <script> import {ref,customRef} from 'vue' export default { name:'Demo', setup(){ // let keyword = ref('hello') //使用Vue准备好的内置ref //自定义一个myRef function myRef(value,delay){ let timer //通过customRef去实现自定义 return customRef((track,trigger)=>{ return{ get(){ track() //告诉Vue这个value值是需要被“追踪”的 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
Child.vue
<template>
<div class="child">
<h2>child(子)</h2>
<Son/>
</div>
</template>
<script>
import Son from "./Son";
import { inject } from "vue";
export default {
name: "Child",
components: {Son},
setup() {
console.log('@@@@ Car ', inject('car'))
}
}
</script>
Son.vue
<template>
<div class="son">
<h2>son(孙)</h2>
<h3>从app组件获取的数据:{{ car.name }} --- {{ car.price }}</h3>
</div>
</template>
<script>
import { inject } from "vue";
export default {
name: "Son",
setup(){
let car = inject('car'); //获取爷爷辈的数据
console.log(car); //响应式数据
return {
car
}
}
}
</script>
App.vue
<template>
<div class="app">
<h2>app(祖) {{ name }}---{{ price }}</h2>
<Child/>
</div>
</template>
<script>
import Child from "./components/Child";
import {reactive, toRefs, provide} from "vue";
export default {
name: 'App',
components: {Child},
setup(){
let car = reactive({
name: 'benz',
price: 40
});
provide('car', car); //给自己的后代组件递数据,只要是后代都可以(后代的后代也可以)
return {
...toRefs(car)
}
}
}
</script>
总结:
作用:实现祖代与后代组件间通信(多级组件的深入通讯)
套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据
具体写法:
- 祖组件中:
jssetup(){ ...... let car = reactive({name:'奔驰',price:'40万'}) provide('car',car) ...... }
- 后代组件中:
jssetup(props, context){ ...... const car = inject('car') return {car} ...... }
6.响应式数据的判断
<template>
<div class="app">
</div>
</template>
<script>
import {reactive, readonly, ref, toRefs, isRef, isReactive, isReadonly, isProxy} from "vue";
export default {
name: 'App',
setup(){
let car = reactive({
name: '宝马',
price: 40
});
let sum = ref(0);
let car2 = readonly(car); ///readonly依然返回代理类型的对象只不过它不能再改而已
console.log(isRef(sum), isReactive(car), isReadonly(car2), isProxy(car), isProxy(car2), isProxy(sum));
}
}
</script>
总结:
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
- isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
- isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
五、Composition API 的优势
1.Options API 存在的问题
Options API:Vue2中的一套,是配置式的API
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改。
因为不是按照功能分类的,而是按照模块分的,所以在修改某一个功能的内容时,需要去这些不同模块中去修改,比较麻烦!
2.Composition API 的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
这里是按照功能进行分类的,一个功能相关的数据、方法、计算属性等都是在一起的!
所以组合式API的最大精髓要通过自定义hook来体现,同样的功能的相关代码就放到一个hook里面!
六、新的组件
1.Fragment(片段、碎片)
- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 原因就是内部会将多个标签包含在一个Fragment虚拟元素中, 最终不参与渲染
- 好处: 减少标签层级, 减小内存占用
2.Teleport(传送、瞬间移动)
App.vue
<template>
<div class="app">
<h2>app</h2>
<Child/>
</div>
</template>
<script>
import Child from "./components/Child";
import {reactive, toRefs, provide} from "vue";
export default {
name: 'App',
components: {Child},
}
</script>
Child.vue
<template>
<div class="child">
<h2>child(子)</h2>
<Son/>
</div>
</template>
<script>
import Son from "./Son";
export default {
name: "Child",
components: {Son},
setup() {
// console.log('@@@@ Car ', inject('car'))
}
}
</script>
Son.vue
<template>
<div class="son">
<h2>son(孙)</h2>
<Dialog/>
</div>
</template>
<script>
import Dialog from "./Dialog";
export default {
name: "Son",
components: {Dialog}
}
</script>
Dialog.vue 重孙组件
<template>
<div>
<button @click="isShow = true">点我弹个窗</button>
<!--如果不用teleport,那么弹窗出现的时候会把外面的三层都撑大,影响效果-->
<!--我们希望弹窗可以出现在屏幕的正中央,而不依赖于任何一个组件的内部!-->
<teleport to="body"> <!--挂载到index.html文件的<body>标签上,会在</body>的上面,这样就可以参照body去写定位了,更好处理定位-->
<div class="mask" v-if="isShow"> <!--遮罩效果弹窗的实现-->
<div class="dialog">
<h3>我是一个弹窗</h3>
<h4>一些内容</h4>
<h4>一些内容</h4>
<h4>一些内容</h4>
<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 scoped>
.mask{ /*遮罩层*/
position: absolute;
top:0;
bottom: 0;
left: 0;
right:0;
background: rgba(0,0,0,.5); /*半透明*/
text-align: center;
}
.dialog{ /*居中的效果*/
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
background: #42b983;
}
</style>
总结:
什么是Teleport?—— Teleport 是一种能够将我们的组件的html结构移动到指定位置的技术。
to = “body” 或者 to = “#id”
语法:
<teleport to="移动位置">
vue<teleport to="移动位置"> <div v-if="isShow" class="mask"> <div class="dialog"> <h3>我是一个弹窗</h3> <button @click="isShow = false">关闭弹窗</button> </div> </div> </teleport>
3.Suspense(悬念、悬而未决)
Child.vue
<template>
<div class="child">
<h2>child(子)</h2>
{{ sum }}
</div>
</template>
<script>
import { ref } from 'vue';
export default {
name: "Child",
//注:这里主要就是为了模拟一个慢的网速,无特殊意义(也可以用浏览器的3g网络模拟)
async setup() { //async是异步的,与await搭配使用
let sum = ref(0);
let p = new Promise((resolve, reject) => { //返回Promise对象值
setTimeout(() => {
resolve({
sum
});
},5000) //等待5s
})
return await p; //await含义:等p成功才返回结果
}
}
</script>
App.vue
<template>
<div class="app">
<h2>app</h2>
<!--Suspense的底层是插槽,default用于展示真正的组件,fallback用于展示组件没出来时的效果-->
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<!--fallback:退路-->
<h3>loading...</h3>
</template>
</Suspense>
</div>
</template>
<script>
// import Child from "./components/Child"; 静态引入组件
import { defineAsyncComponent } from "vue"; //定义异步组件
const Child = defineAsyncComponent(() => import('./components/Child')); //动态引入组件(异步)
export default {
name: 'App',
components: {Child},
}
</script>
总结:
Suspense的作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验(比如加载圈等)
静态引入:父组件和子组件会一起渲染(如果子组件没加载出来,父组件也没办法加载)——>这是一般的情况
动态引入(异步引入):父组件会先加载,再加载子组件 ——>这是使用了defineAsyncComponent的效果(定义异步组件)
使用步骤:
异步引入组件
jsimport {defineAsyncComponent} from 'vue' const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
使用
Suspense
包裹组件,并配置好default
与fallback
两个插槽(固定配置)vue<template> <div class="app"> <h3>我是App组件</h3> <Suspense> <!--标签里边是异步组件--> <template v-slot:default> <Child/> </template> <template v-slot:fallback> <!--Child组件未加载出来时显示这个--> <h3>加载中.....</h3> </template> </Suspense> </div> </template>
七、其他
1.全局API的转移
Vue 2.x 有许多全局 API 和配置。
例如:注册全局组件、注册全局指令等。
js//注册全局组件 Vue.component('MyButton', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>' }) //注册全局指令 Vue.directive('focus', { inserted: el => el.focus() }
Vue3.0(没有了Vue这个类)中对这些API做出了调整:
将全局的API,即:
Vue.xxx
调整到应用实例(app
)上对应关系:
注:
1.vue3里面在开发环境时默认把生产提示关闭了,并且移除了这个配置项!
因为现在的脚手架可以分辨出来我们所处的环境npm run serve/build,所以就不会有这个提示了
2.现在依然能用全局属性(所有vc对象都可以获取到的),但是方法变成了globalProperties
2.其他改变
data选项应始终被声明为一个函数!
过度类名的更改:
Vue2.x写法
css.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }
Vue3.x写法
在来的时候加了一个-from
css.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes
(1)移除keyCode 即不支持@keyup.13,不支持按键编码绑定事件,因为这个兼容性比较差
(2)移除config.keyCodes 即不支持按键别名,因为这个也要依赖按键编码
移除v-on.native修饰符
在组件标签上使用自定义事件时,使用.native标志这个事件是一个原生事件
即
@click.native="hello"
那vue3中该如何标志原生事件呢?
父组件中绑定事件
vue<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" />
子组件中声明自定义事件
vue<script> export default { emits: ['close'] // 声明指定自定义事件,制定了的就是自定义事件 //那么没声明的就是原生事件,所以click默认就是原生事件 } </script>
移除过滤器(filter)
官方解释:过滤器虽然看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
推荐用计算属性或者普通方法去实现!
- …
3.vue3.2更新内容
1、不需要再return即可使用变量和方法,可以直接使用组件名
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
<!--动态组件-->
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
2、在css样式中可以使用v-bind绑定变量
<style scoped>
button {
color: v-bind(color);
}
</style>
3、新增 defineCustomElement方法(自定义html元素)
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// normal Vue component options here
})
// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)
4、新的指令 v-memo 比如一些不改变的数据跳过更新