需要vue-cli
版本在4.5以上,创建的步骤和vue2的过程基本一样,只需要选择适配Vue3的环境即可。
创建过程如下
npm init vite-app 工程名
创建工程,(这个时候vue3工程文件中没有依赖包)
cd 工程文件
进入工程文件中操作
npm install (or
yarn)
安装依赖
npm run dev (or
yarn dev)
运行,在package中将serve修改为dev
npm init vue@latest
使用vite创建一个Vue项目,该命令会自动安装和执行create-vue,vue官方提供的脚手架工具。安装完成后会有步骤提示操作。
cd 项目文件名
npm install
npm run dev
这里对比的都是采用vue create 工程名
方法创建的脚手架区别。
在Vue3中当创建完成一个脚手架后,可以查看入口文件main.js
文件中的默认代码如下。和Vue2区别还是很大的,不再采用render
函数去去渲染。
在这里不再引入Vue构造函数,并且这里也不能引入import Vue from 'vue
,因为这里进行了处理,获取的Vue构造函数是undefined,所以无法进行实例化new操作。
引入一个名为createApp
的工厂函数,我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
分解步骤
第一步类似Vue2中的new Vue构造函数,创建的app类似vm实例,但是相比于之前的vm,更加轻量化,将Vue2之前的vm身上不用的东西都去除。
const app = createApp(App)
第二步类似Vue2的el,只不过在这里换成了mount,用于渲染容器
app.mount('#app')
/* Vue2生成的代码 */
// const vm = new Vue({
// render: h => h(App)
// })
// vm.$mount('#app')
通过打印输出发现createApp(App)
的结果如下,不仅有mount
挂载方法也有unmount
卸载方法
其次还有一个区别就是在Vue3的组件模板结构template
中,不再有根标签的限制了。
Vue3中所有的data都是函数式
setup函数
是Vue3提供的新配置项,值为一个函数。
setup
是所有Composition API
的平台。
组件中所有用到的数据,方法等,均要配置在setup
中。
setup
函数的两种返回值
setup() {
let name = "zq"; //直接声明使用,不需要使用如data,methods等关键字
function getInfo() {
alert(`个人信息:${name}`); //函数作用域直接使用
}
return {
name,
getInfo,
};
},
以下是setup返回渲染函数,不常使用
import { h } from "vue";
return () => {
return h("h1", "我很牛"); //需要将h渲染函数的返回值交给setup函数的返回值
};
特别注意的,在Vue3中尽量不要写Vue2相关的代码。如果一定要写,也请看下面的代码按照约定去写。
Vue2的配置项data,methods等可以访问到vue3中setup
的属性和方法
但在vue3的setup
中不能访问到vue2中data,methods等属性和方法
当出现同同名属性的时候,以Vue3为主
当同时写了vue2和vue3的代码的时候,**在不互相交叉使用的时候,**在Vue3中可以正确显示vue2的内容。
name: "App",
data() {
return {
year: 2023,
};
},
methods: {
getYear() {
alert(`今年是${this.year}年`);
},
},
setup() {
let name = "zq";
function getInfo() {
alert(`个人信息:${name}`);
}
return {
name,
getInfo,
};
},
data() {
return {
year: 2023,
};
},
methods: {
// 交叉使用vue3的数据,可以正常显示页面
getYear() {
this.getInfo();
alert(`今年是${this.year}年,后面是vue3的属性姓名:${this.name}`);
},
},
setup() {
let name = "zq";
function getInfo() {
alert(`个人信息:${name}`);
}
/* 在vue3中交叉获取vue2的数据,页面报错,注意this指向 */
function getVue2() {
this.getYear();
alert(`获取vue2的信息:${this.year}`);
}
return {
name,
getInfo,
getVue2,
};
},
setup
函数不能写成一个async
函数,是因为async
的返回值是加工后的promise
,所以的内容都在promise
中需要then
调用取出,这就会导致模板看不见其中的内容
setup函数执行的时机比beforeCreate
生命周期函数早,并且this为undefined
props
:前提创建一个App组件和Demo组件,并且在App组件中使用Demo组件标签并传递数据,但是在Demo组件中没有设置props
配置项接收数组,那么这些组件中传递的值怎么查看
通过打印组件实例对象,发现所有没有设置props
接收的属性,最终都会出现在$attrs
上(目前没有设置props:["msg","Vue"]
)。
当设置了 props: ["msg"],
只接收一个参数,打印如下。没有被配置项props
接收的参数,最终都出现在$attars
属性上。
当在组件标签中添加了HTML原元素的时候,但是没有使用插槽占位。那么那些HTML元素去哪里了。
所有没有使用slot
标签占位的元素,最终都被组件实例对象身上的$slots
属性收集到,且收集到的是虚拟DOM。
前提知识,在Vue3的setup
函数,会默认接收两个参数,分别为props
和context
setup(props, context) { console.log(props, context);}
在这里也是首先搭建一个App组件和Demo组件,并在App中使用Demo组件标签传递参数,但是并不在Demo组件中使用props
配置项接收。
<Demo msg="Hello"></Demo>
Vue3会给出提示:存在一个使用组件标签传递参数的情况,但是并没有配置props
配置项接收,只要配置了props
,该提示就会消失。并且,哪一个传递来的属性没有被props
接收,Vue3会灵活的提示还有哪些属性没有接收。
凡是传递来的参数被props
配置项接收了,最终都会出现在setup
函数的第一个参数上,并且所有接收来的数据都为Proxy
响应式的
并且这里会跟Vue2一样,将没有接收的数据,存放在一个地方,这个时候就用到了setup
的第二个参数,该参数中有attrs
,emit
,slots
属性,功能和Vue2的一样。
其中attrs
属性接收所有没有被props
配置项接收的数据,全部存放在这里
当在组件标签中添加HTML元素的时候,这个时候就需要使用插槽了
如果使用了插槽slot标签,打印setup
的第二个参数身上的slots
属性查看
<slot></slot>
。。。。
console.log(context.slots);
如果使用了命名插槽,那么defalut就会显示命名的插槽名(必须使用Vue3的语法:v-slot:
)
当给组件标签定义了一个自定义事件,如果直接在另一个组件中触发该事件就会报错提示
<Demo msg="Hello" sub="Vue" @say="sayHello"> </Demo>
-------------------------------------------------------
//触发自定义事件的组件中
function say() {
context.emit("say", 666);
}
这是因为在Vue3中对于父组件给子组件绑定了一个自定义事件。需要子组件配置一个emits
配置项,其值为数组,接收传来的事件名。
emits: ["say"],
setup(props, context) {
let person = reactive({
name: ref("张三"),
age: ref(18),
});
function say() {
context.emit("say", 666);
}
return {
person,
say,
};
总结
props
配置中声明的属性,等于vue2的this.$attrs基本代码如下,当尝试调用setNewInfo
修改信息的时候,发现Vue并没有监测到数据的改变,从而进行响应式布局,也就是说不能直接进行如下的定义声明和修改步骤
let name = "张三";
let age = 18;
function setNewInfo() {
(name = "李四"), (age = 22);
console.log(name, age);
}
为了解决此问题,Vue3提供了全新的API即ref
函数,接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
如果想使用ref
API,就必须引入import { ref } from "vue";
import { ref } from "vue";
......
let name = ref("张三");
let age = ref(18);
function setNewInfo() {
console.log(name, age);
}
.....
经过打印输出,发现一个简单的字符串和数值被包装成一个对象。其中红色区域是由RefImpl
构造函数实例化出来的对象。RefImpl
全称Reference Implement
,使用该构造函数实例化出来的对象叫引用实现的实例对象。在ref
底层也是基于ObjectDefineProperty
实现响应式的。对于ref
的值进行了响应式处理,将值全部进行了代理放在__proto__
身上,其主要目的是显示的时候看起来很简化,将不用的东西全部隐藏。
如下代码中,用户对某一个引用实现的实例对象进行.value
操作的时候,就可以被Vue监测到从而进行响应式。
如下修改,即可实现简单的响应式
let name = ref("张三");
let age = ref(18);
function setNewInfo() {
name.value = "李四";
age.value = 30;
}
但是在页面中使用的时候,就不需要进行.value
的操作,可以简单的理解Vue为了能帮助我们简化操作,当解析到name
的时候,就明白这是一个RefImpl
的实例对象,于是就自动的去执行name.value
取出值显示在页面中。
<h1>姓名:{{ name }}h1>
<h1>年龄:{{ age }}h1>
添加如下代码块
let job = ref({
jobName: "前端程序员",
salary: "1999",
});
....事件回调函数中打印输出
console.log(job);
....
输出job的输出结果可以发现,当job
使用了ref
函数的时候,其本质就会去new一个 RefImpl
构造函数,所以传入ref
中的对象理应在value
属性中。那么执行到这里根据Vue2的思路,vue会对于一个对象解构,不断的循环遍历,为每一个数据都自动的添加上响应式。但是事实却不是这样子的。
根据上面分析添加下面的代码。一旦添加下面的代码了,程序就会报错
job.value.jobName.value = "后端程序员";
在报错中给出提示·,不能在一个字符串身上创建新的属性。即不能在jobName
属性上继续调用.value
继续打印观察job.value
中给的内容是什么,结果如下,value的形式为Proxy对象
,这是ES6新增的数据代理Proxy代理
。发现打印的结果显示:在value
中,可以直接使用那些对象中属性。
console.log(job.value);
修改代码如下,发现页面可以实现响应式效果
job.value.jobName = "后端程序员";
job.value.salary = 2000;
在Vue3中,对于一些基本的数据类型,采用的是ObjectDefineProperty
实现数据代理。但是对于对象类型的数据,对于其引用的地址也是基于ObjectDefineProperty
实现,但是对于引用地址里面的值,是基于Proxy
实现数据代理的。Vue3对于不同类型的数据采用不同的数据代理方式。
reactive
函数,该函数实现对proxy
的封装。对象中的属性都会交给reactive
函数的处理,最终称为proxy
对象的形式。作用:给对象类型的数据进行数据代理,实现响应式效果。
重点:基本数据类型不能使用reactive
函数,只能使用ref
函数
reactive
函数是基于Proxy
实现数据代理,Proxy
的数据代理能够遍历深层次的对象解构
let 代理对象 = reactive({源对象}) //可以给对象或数组进行代理,返回的是Proxy的实例对象
如果想使用reactive
实现数据代理,就必须引入
import { reactive } from "vue";
验证reactive
不能给基本数据类型设置,如下图。一旦将一个基本数据类型交由reactive
进行处理,Vue会立即报错提示。
将上面的对象从ref
函数改为reactive
函数,并打印输出,也就是说在修改数据的时候,可以直接job.属性
从而修改数据,而修改数据后的结果会被vue监视到从而重新布局页面。
let job = reactive({
jobName: "前端程序员",
salary: 1999,
});
....
console.log(job);
....
job.jobName = "后端程序员";
job.salary = 2000;
对比ref
的对象操作,发现其实本质是一样的,ref
函数在处理对象的时候,底层会调用reactive
函数处理,所以在返回的Proxy
对象中可以直接调用并修改。开发中对于对象的数据监测使用reactive
的原因是可以简化一些代码。如果是ref
处理,则ref
会先返回一个RefImpl
对象。其中所以的参数都存放在value
属性中,从而造成了必须再次.value
后才能获取返回的Proxy
对象,获取值。相比于reactive
,每一步都多了一个.value
步骤。
reactive
也能实现对数组的代理。
let year = reactive(["2001", "2002", "2003"]);
定义角度
*:ref
通常定义基本数据类型
*:reactivev
通常用来定义对象和数组类型数据
*:ref
也可以用来定义对象和数组类型的数据,其底层会自动调用reactive
函数
从原理角度
*. ref
通过Object.defineProperty
方法的get
和set
实现响应式。
*:reactive
通过Proxy
实现响应式。并通过Refelct
操作源对象内部数据
从使用角度
*.ref
定义的数据需要.value
才能读取数据
*:reactive
定义的数据,操作和读取数据均不需要.value
对象类型基于Object.DefineProerty
对属性的读取,修改,拦截,,代理。
数组类型基于重新更写数组的一些列方法来实现数据拦截代理。对于数组的原方法进行了包装
但是Vue2的响应式存在一些问题造成了响应式的不方便。
Vue.set()
或this.$set()
对新增的属性设置响应式。Vue.delete()
或this.$delete()
实现对象属性的删除。先简单写一段Vue2的响应式代码。在控制台对p进行增删查改,可以发现修改某个数据的时候,页面监测到并成功更新。但是删除某个数据或添加新的数据的时候,页面不能及时更新。这就是Object.DefineProerty
带来的问题。不能同时对一个对象中的所有属性进行劫持代理,只能循环代及其不方便。且问题很多。
<h1 id="name">姓名:</h1>
<h1 id="age">年龄:</h1>
<script>
let DomObj = {
name: document.querySelector('#name'),
age: document.querySelector('#age')
}
let per = {
name: '张三',
age: 18
}
DomObj.name.innerHTML = per.name
DomObj.age.innerHTML = per.age
let p = {}
Object.defineProperty(p, 'name', {
configurable: true,
get() {
return per.name
},
set(value) {
per.name = value
DomObj.name.innerHTML = per.name
}
})
Object.defineProperty(p, 'age', {
configurable: true,
get() {
return per.age
},
set(value) {
per.age = value
DomObj.age.innerHTML = per.age
}
})
但在Vue3中,对于Vue2的问题都被解决了,可以直接修改某个对象的值,并且添加数据的时候不需要添加过多的操作。
let per = {
name: '张三',
age: 18
}
//p为代理对象,per为源对象
let p = new Proxy(per, {
get(target, key) { //target就是per对象,key为字符串类型的属性名
console.log("读取", target, key)
return target[key]
},
set(target, key, value) {
console.log("修改", target, key, value)
target[key] = value
}
})
这里需要注意的是当添加一个新的属性的时候,走的是Proxy中的set
函数,也就是说set
函数能够实现修改数据和添加数据两个功能
set(target, key, value) {
console.log("修改、添加", target, key, value)
target[key] = value
}
当需要修改的时候 Proxy
提供了deleteProperty
方法来实现删除数据并监测,该方法接收一个返回值,通常在该方法中将delete targert[value]
的结果布尔值作为返回值。当调用delete
删某个属性的时候,就会自动去执行deleteProperty
方法。
deleteProperty(target, key) {
return delete target[key]
}
在Vue3中,get
函数实现读取的功能,set
函数实现修改和添加的功能,deleteProperty
实现删除效果。
作用“:对源对象的某个属性进行操作。
Reflect并不是一个构造函数,不能使用new关键字
如下常见的Reflect
静态方法。
Reflect.get(target, key) //返回获取到的值
Reflect.set(target, key, value) //返回布尔值,表示成功设置或许修改key的值。如果多次多某个key进行设置,则以第一次为主。
//Object.defineProperty不能重复设置同一个key,否则会报错
Reflect.deleteProperty(target, key) //返回布尔值
使用Reflect
可以让程序更加灵活健壮。由于设置失败不会报错,而是通过返回的布尔值进行相应的操作,对比Object.defineProperty
设置失败就报错有很大提升。
Vue3虽然向下兼容Vu2计算属性的写法,但是不推荐在Vue3中使用Vue2的写法
Vue3中要想使用计算属性computed
,就必须引入该API。
import { computed } from "vue";
因为是组合式API,所以需要放在setup
函数中,并且在调用的时候使用computed()
。
当computed()
参数传递一个函数的时候。就是简写形式,即只有get
的可读情况。
setup() {
。。。。。
let fullName = computed(() => {
return person.firstName + "-" + person.lastName;
});
return {
fullName,
};
即在computed
API中传入配置对象,对应的写法和Vue2一样
setup(){
......
let fullName = computed({
get() {
return person.firstName + "-" + person.lastName;
},
set(value) {
let arr = value.split("-");
person.firstName = arr[0];
person.lastName = arr[1];
},
});
.....
}
在Vue3中可以直接使用Vue2的watch
配置项进行侦听。(不需要引入watch的API,且不写在setup函数中)
watch: {
sum(newV, oldV) {
console.log("sum修改了了", newV, oldV);
},
},
watch: {
sum: {
immediate: true,
handler(newV, oldV) {
console.log("sum修改了了", newV, oldV);
},
},
},
Vue3中要想使用自己的watch
,就必须引入该API,并且需要写在setup
函数中
import { watch } from "vue";
在Vue3中将watch
设置为一个API,该方法接收三个参数:watch(监视的属性,执行的回调函数,设置配置项)
当监视的参数为单个属性的时候,在回调函数中接收的新值和旧值均为简单数据类型
setup() {
let sum = ref(0);
watch(sum, (nv, ov) => {
console.log("sum", nv, ov);
});
return {
sum,
}
当监视的属性为多个的时候,在回调函数中接收的新值和旧值为数组
setup() {
let sum = ref(0);
let msg = ref("你好");
watch([sum, msg],(nv, ov) => {
console.log("sum", nv, ov);
}, { immediate: true }); //配置watch的第三个参数
return {
sum,
msg,
};
},
修改msg的时候,被watch监视,从而打印新值和旧值,发现每一个新值和旧值都包含sum和msg组成的数组。
当使用reactive
定义一个对象的时候。会发现打印的newValue和oldValue打印如下,这个地方出现一个问题,即新值和旧值的结果都以新值为主,即使使用ref
定义一个对象,结果也一样,因为ref
处理对象也是底层调用reactive
实现。且ref
声明的对象,在监视的时候需要使用person.value
使用reactive
定义一个对象Person的时候,无法获取旧值,且该监视是强制开启深度遍历。设置deep:false
无效
let person = reactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
watch(person, (newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
});
下面是修改jobName
的情况,发现没有设置deep:true
的时候,也能监视深层次的数据。即使设置为false也能监测。
当使用readtive
监视一个对象的某个属性的时候,发现Vue直接给出提示。也就是说无法直接监视一个对象的某个属性,那么最简单的修改就是将监视的值包装成一个箭头函数返回。
watch(person.name, (newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
});
将一个普通的对象属性放在箭头函数中的返回值中返回
watch(() => person.name,(newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
}
使用reactive
监视对象中的某些属性,就是将对象中的普通属性,放在箭头函数中的返回值,并将箭头函数存在一个数组中
watch([() => person.name, () => person.job.j1.jobName],(newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
}
);
当使用reactive
监视一个对象中的某个对象,则deep配置项有效
。以下代码将job对象放在一个函数中返回,所以可以理解为一个新的引用,不再是reactive,需要开启深度监视deep。
watch(() => person.job,(newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
},{ deep: true }
);
但是如下代码设置deep
配置项就无效
watch(person.job, (newValue, oldValue) => {
console.log("person监视到了", newValue, oldValue);
});
两个小“坑”:
- 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效。
-
//情况一:监视ref定义的响应式数据
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
},{immediate:true})
//情况二:监视多个ref定义的响应式数据
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变化了',newValue,oldValue)
})
/* 情况三:监视reactive定义的响应式数据
若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
若watch监视的是reactive定义的响应式数据,则强制开启了深度监视
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不再奏效
//情况四:监视reactive定义的响应式数据中的某个属性
watch(()=>person.name,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true})
//情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.age,()=>person.name],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true})
//特殊情况
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
ref
设置一个简单数据类型的时候在配置wathc
监视的时候,使用 .value
,这个时候Vue就给出了报错提示说明:如下,简而言之,就是sum.value
的值即0已经不是一个ref
结构的数据了,而是一个普通的数值,如果是这样子那么就需要采用函数返回的形式返回该基本数据类型。即 () => sum.value,
let sum = ref(0);
.......
watch(sum.value, (nv, ov) => {
console.log("sum被监视到了", nv, ov);
});
ref
定义的对象的时候出现的问题在如下代码中person
对象使用ref
定义。并且为该对象设置了监视,结果测试控制台打印,发现该对象的监视并没有成功,这是什么原因?
let person = ref({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
watch(person, (nv, ov) => {
console.log("person被监视到了", nv, ov);
});
person
为一个ref
定义的对象,即没有开启深度遍历的对象
1.第一种解决办法就是直接添加deep
配置项。当一个对象中某个数据改变的时候,该对象内部是不被监视到的。如let obj = {a:1} 当修改其内部a的值,不被监视,而一旦修改obj的地址,就会被监视到。
watch(person,(nv, ov) => {
console.log("person被监视到了", nv, ov);
},{ deep: true }
);
reactive
函数,这里需要知道,在ref
声明的person
对象结构中,所有的数据都是存放在value
中,而该对象是通过reactive
实现的,而reactive
监视的对象是强制开启深度遍历的。 watch(person.value, (nv, ov) => {
console.log("person被监视到了", nv, ov);
});
wathcEffect
函数的出现,解决了watch
配置的繁琐问题。
watchEffect
函数更加智能,能够自行分辨哪些数据是否监视。即在该函数中用到的数据均会被监视
watchEffect
也属于组合式API,所以需要引入。
在调用的时候直接使用watchEffect(()=>{})
import { watchEffect } from "vue";
........................
let person = reactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
watchEffect(() => {
console.log("wathcEffect调用了");
});
............................
但是按照如上代码,没有点击页面,控制台自动打印wathcEffect调用了。也就是说watchEffect
默认开启了immediate:true
,即使点击了页面,watchEffect
函数也不会调用。(这是因为该函数中没有所依赖的数据。)
当在wathchEffect
函数中配置了一些vm实例身上的属性的时候,该函数就能自动的获取并配置哪些数据是需要被监视的,且对于其他没有出现在内部的数据,不会进行处理。
watchEffect(() => {
let a = sum.value; //sum.value被监视
let b = person.job.j1.jobName; //person.job.j1.jobName被监视
//其他属性均不会被监视
console.log("wathcEffect调用了");
});
watch
要指明监视的属性和监视所需要执行的回调函数watchEffect
不需要指明哪些属性,监视的回调中用到哪些属性,就监视那个属性watchEffect
类似计算属性computed
。但是计算属性注重返回值中计算出来的结果。而watchEffect
注重监视的过程。以下的Vue3生命周期图是旧版
以下这个是新版的vue3生命周期图,目前先理解上面的旧版,本质两个图是一样的
通过配置项形式使用生命周期钩子,需要区别的是在Vue3中,不在有销毁这个概念了,取而代之的是对立的挂载和卸载,也就是说最后两个钩子修改了,从原先的destroy均换成了unmounted。如果在vue3中使用了vue2相关的destroy销毁函数,则无效。
以下是配置形式,同时还有组合式API的格式,但是写成组合式API的生命周期函数,需要在setup
函数入口中写,且每一个生命周期函数都需要更换名字。
setup() {
let sum = ref(0);
return {
sum,
};
},
beforeCreate() {
console.log("--beforeCreate");
},
created() {
console.log("--created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("muonted");
},
beforeUpdate() {
console.log("beforeUpdata");
},
updated() {
console.log("updated");
},
beforeUnmount() {
console.log("beforeUnmount");
},
unmounted() {
console.log("unmounted");
},
};
因为生命周期函数采用组合式API方式使用,所以需要引入。每一个都需要传入一个回调函数作为参数进行调用
import {onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from "vue";
--------------
setup(){
//调用
onBeforeMount(()=>{})
}
需要注意的是在Vue3中,如果使用组合式API的形式,那么就不存在beforeCreate
和created
这两个生命周期函数,这两个函数都被一个setup函数集合在一起
。其余的生命周期函数不变只是在首部均添加上了on
表明是在setup
函数内使用的生命周期函数。
如果组合式的生命周期函数和配置项形式的生命周期函数一起出现,则在setup
中的生命周期函数优先级高,会先执行。
生命周期函数可以多次调用,会按照代码中的位置顺序执行,主要作用是:如果接收一个别的项目,其onMounted函数中实现了很多逻辑功能,你不确定是否可以修改,但是需要添加你自己的功能代码,这个时候就可以再次写一个onMounted函数。
hook本质是一个函数,将setup
函数中使用的Composition API组合式API进行了封装。
作用:将一些需要复用的功能,代码,组合式API封装起来使用。
类似Vue2的mixin
混入。
使用步骤:1:通常在src
文件中创建一个新的hook
文件夹,里面存放封装好的文件,建议取名字的时候使用如useName
这种形式创建,让人见者知其意。(如果在hook函数中封装了一些数据,而这些数据在组件中需要被使用到,那么需要在hook函数中将数据返回出去)
如下代码,在一个组件中使用获取鼠标坐标的功能,但是当另一个人也需要使用该功能的时候该如何处理,不能将代码复制过去,必须实现代码复用才行,于是自定义hook
函数的出现了。
setup() {
let point = reactive({
x: 0,
y: 0,
});
function getPoint(e) {
point.x = e.pageX;
point.y = e.pageY;
}
onBeforeMount(() => {
window.addEventListener("click", getPoint);
});
onBeforeUnmount(() => {
window.removeEventListener("click", getPoint);
});
return {
point,
};
},
将代码全部封在一个文件中
import { onBeforeMount, reactive, onBeforeUnmount } from 'vue'
// hook本质是一个函数
// 需要暴露外部使用
export default function () {
let point = reactive({
x: 0,
y: 0,
});
function getPoint(e) {
console.log("执行了")
point.x = e.pageX;
point.y = e.pageY;
}
onBeforeMount(() => {
window.addEventListener("click", getPoint);
});
onBeforeUnmount(() => {
window.removeEventListener("click", getPoint);
});
// 将内部数据返回
return point
}
在用到该方法组件中引入
// 引入自定义hook函数
import usePoint from "../hook/usePoint";
----------
let point = usePoint(); //需要用到内部数据,即point,所以需要返回值
----------
toRef
也是组合式API中的一个,所以也需要单独引入
作用:创建一个ref
对象,其value
值指向了另一个对象中的某个属性。将一个响应式对象中层次很多的属性提出来并提供给外面使用且不丢失响应式效果。
语法:const xxx = toRef(对象,‘该对象中的属性’)
当在setup
中有一个结构及其复杂的对象,并将该对象不加以任何处理直接return返回的时候。如果想在模板中使用,那么就必须一层一层的.
调用下去。
但是按照上面的写法,就违背了vue的推荐做法,将模板中的代码显的即为复杂。为了方便,首先想到的办法就是在return中进行处理。但是这么处理就会丢失响应式效果,将该对象中的某个属性取出,相当于将一个字符串或数值取出,这样子显然是没有响应式的。比如person.name就是‘张三’,一个字符串赋值给name属性是没有响应式效果的。
如下两副图解释
import { toRef } from "vue";
基本结构如下,观看打印输出的两种结果区别。第一个将一个对象中的某个属性直接取出,丢失了响应式效果。而第二个考虑很多,明知到取出来会丢失响应式效果,于是主动再次添加上响应式即可,再次转换为ref
对象即可。
这里需要重点记忆:toRef
中监视该对象中的name
属性,实则监视的是原reactive
中的name
。只需要证明修改x2的name的时候person中的name也修改即可。
setup() {
let person = reactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
let x1 = person.name;
let x2 = toRef(person, "name");
console.log(x1);
console.log(x2);
。。。。。。。。。。。。。。。。。。省略
},
为了验证上面的结论,我们修改代码如下,点击按钮的时候修改的是toRef
处理的数据,对比结果发现,当toRef
处理过后的name
和age
值改变的时候,person
对象中的数据也跟着改变了。
//结构代码
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>工作:{{ jobName }}</h2>
<hr />
<h3>我是person中的姓名:{{ person.name }}</h3>
<h3>我是person中的年龄:{{ person.age }}</h3>
<button @click="name += '~'">点击姓名</button>
<button @click="age++">点击年龄</button>
<button @click="jobName += '!'">点击增加</button>
----------------------
return {
/* toRef */
name: toRef(person, "name"),
age: toRef(person, "age"),
jobName: toRef(person.job.j1, "jobName"),
/* 源数据 */
person,
};
注意不能使用ref
去重新定义数据,因为ref
会将该数据取出后,重新定义为一个对象,源person
中的数据无法与ref
重新定义后的数据保持同步
return {
/* ref */
name: ref(person.name),
age: ref(person.age),
jobName: ref(person.job.j1.jobName),
/* 源数据 */
person,
};
},
作用和toRef
一样,区别是toRef
只能单独处理一个数据,而toRefs
能同时处理多个。
格式:const xxx = toRefs(对象)
将以下代码打印输出结果如下 let x = toRefs(person);
发现其对每一个一级属性都进行了处理,全部转换为RefImpl
引用实现实例对象处理。
代码如下,将toRefs
返回的对象展开,取出里面的值,里面所以的值均经过Proxy
代理到了person
对象上。也就是说,toRefs
中的代理值改变,person
中的值也会跟着改变,从而达到了数据统一的目的。
return {
/* toRefs */`在这里插入代码片`
...toRefs(person),
/* 源数据 */
person,
};
shallowReactive
:只处理对象最外层属性的响应式(浅响应式)
shallowRef
:只处理基本数据类型的响应式,不进行对象的响应式处理。
使用时机
shallowReactive
shallowRef
shallowRef
,shallowReactive
最外的数据是响应式,区别是对内部数据如何处理因为 shallowRef
,shallowReactive
都是组合式API,所以需要从vue中引入
import { shallowReactive, shallowRef } from "vue";
基本代码如下
setup() {
let person = shallowReactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
return {
/* 源数据 */
person,
};
},
页面中的每一个按钮都点击查看效果,发现在person对象中其job对象中里面的j1对象中的数据未进行改变。即shallowReactive
:只处理对象最外层属性的响应式
当对一个基本数据类型使用shallowRef
定义的时候,页面能够正常处理响应式
let x = shallowRef(0);
当使用shallowRef
定义一个对象的时候,发现该内部的x.data失去了响应式。(但是x还有具有响应式的,当整个对象地址改变的时候,页面变化,即将x修改为一个新的对象,x={…})
let x = shallowRef({
data: 0,
});
consolo.log(x)
打印shallowRef
定义的x如下,发现x是响应式的RefImpl
,但是内部的数据是一个简单的Object对象,所以无响应式。
readonly
:让一个响应式的数据变为只读,(多层次对象的全部属性变为只读,深只读)shallowReadonly
:让一个响应式的数据变为只读。(若为对象,只针对最外层的属性设置为只读,浅只读)应用场景:当多个组件共享一个响应式数据的时候,希望其中的某个组件允许用该响应式数据,而不允许修改(即接收到该响应式数据的时候立即设置为只读),但是其中组件可以对该响应式数据进行修改。
import {readonly, shallowReadonly } from "vue";
基本代码如下,将原先是reactive
声明的响应式对象person
设置为只读,调用readonly(person)
将响应式对象person作为参数传入该方法中。返回新的person供外部使用。
当点击页面按钮的时候,会发现Vue监测到你想修改只读的数据类型,于是控制台给出警告
setup() {
let sum = ref(0);
let person = reactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
person = readonly(person);
当改为shallowReadonly(person)
的时候,意思是将该属性的最外层设置为只读,深层次的依旧是响应式的。如图当点击页面的时候,j1对象中的数据依旧为响应式
person = shallowReadonly(person);
readonly, shallowReadonly
不仅可以将reactive
定义的对象设置为对应的只读,也可以将ref
定义的简单数据类型设置为只读。
但是shallowReadonly
处理ref
定义的对象的时候,该对象最外层数据依旧为响应式
作用:将一个由reactive
生成的响应式对象转为普通对象。对ref
定义的数据无效
场景:读取响应式对象对应的普通对象,对这个普通对象的所有操作不会引起页面更新。
引入该API
import {toRaw } from "vue";
基本代码如下,person
源对象是一个readtive
定义的响应式数据,在函数中使用toRaw
方法将该响应式对象转换为普通对象,使其失去了响应式
let person = reactive({
name: "张三",
age: 33,
job: {
j1: {
jobName: "Vue",
},
},
});
function addRaw() {
let p = toRaw(person);
console.log(p);
}
作用:标记一个对象,使其永远不会变为响应式数据。
场景:有一个很复杂的对象,如果让vue渲染一个几乎不改变的对象数据,跳过响应式转换处理可以提高性能。
引入该API
import { markRaw } from "vue";
如果初始的时候定义了一个响应式对象,后期突然想给该对象添加一个数据,该数据很复杂且基本不变,那么就可以给该数据打一个标记,告诉vue这个数据不需要进行响应式proxy处理,跳过代理操作,提高性能。(proxy代理的对象添加的数据也是响应式的)
function addRaw() {
let car = { carName: "宝马" };
person.car = markRaw(car);
}
上面的代码给car对象打了一个标记,告诉vue对该数据不需要进行响应式处理。所以当点击页面的时候不会改变该数据
作用:建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
vue中ref
相当于一个精心封装的函数,其内部功能齐全,而customRef
是只有部分功能,可以自己根据需求完善。即使是自定义ref,也不可能自己手写,需要借助customRef
方法
import {customRef} from "vue";
在自定义的ref
中,需要将调用的customRef
方法的结果返回
// 自定义一个ref
function myRef(value) {
return customRef();
}
let msg = myRef("hello");
但是这么使用customRef
方法就会报错,vue要求,该方法必须传入一个函数作为参数
但是当设置完函数作为参数后,控制台还给出了报错信息,vue要求该函数必须返回一个对象
function myRef(value) {
return customRef(() => {});
}
这样子设置返回对象后,控制台还是给出报错信息,即该函数的返回值对象中需要包含get函数
和set函数
function myRef(value) {
return customRef(() => {
return {};
});
}
配置完成如下,其中也是基于响应式,这是因为自定义的ref
数据也需要遵循响应式。
其中get
是读取的时候调用,set
是修改的时候调用
function myRef(value) {
return customRef(() => {
return {
get() {},
set() {},
};
});
}
在customRef
的参数中,该回调函数接收两个值分别为:track跟踪方法
和 trigger触发方法
track跟踪方法
作用:告诉get函数,追踪自定义ref
中值的变化。否则即使值改变,get函数也不会重新执行将值返回
trigger触发方法
作用:每次修改自定义ref
函数中的值的时候,都需要在set
中去调用trigger()
重新渲染模板。
修改值的时候执行顺序:先执行set函数将value的值修改为新值,调用trigger方法重新渲染模板,当渲染模板的时候,需要再次读取msg的值,于是又进入get函数中读取值,因为get函数中有track函数。于是get函数将return的返回值作为msg的最终值。
// 自定义一个ref
function myRef(value) {
return customRef((track, trigger) => {
return {
get() {
console.log("get读取了");
track(); //必须放在这里,告诉vue需要跟踪value值的变化
return value;
},
set(newValue) {
console.log("set修改了", newValue);
value = newValue; //重新修改value的值
trigger(); //通知vue重新解析模板数据
},
};
});
}
let msg = myRef("hello");
将功能代码修改如下,设置输入值后延迟1秒显示,注意防抖
如果这么写代码会有bug,每次输入都会都会开启一个新的定时器,上一个trigger函数调用后,会重新渲染模板,导致bug出现
set(newValue) {
console.log("set修改了", newValue);
// 设置输入值后延迟1秒显示,注意防抖
setTimeout(() => {
value = newValue; //重新修改value的值
trigger(); //通知vue重新解析模板数据
}, 500);
},
正确做法就是采用防抖,每次开启新的定时器之前,清除上一个开启的定时器
// 自定义一个ref
function myRef(value, delay) {
let timer;
return customRef((track, trigger) => {
return {
get() {
console.log("get读取了");
track(); //必须放在这里,告诉vue需要跟踪value值的变化
return value;
},
set(newValue) {
console.log("set修改了", newValue);
// 清除定时器
clearTimeout(timer);
// 设置输入值后延迟1秒显示,注意防抖
timer = setTimeout(() => {
value = newValue; //重新修改value的值
trigger(); //通知vue重新解析模板数据
}, delay);
},
};
});
}
let msg = myRef("hello", 500);
这两个API同样是组合式API,他们的作用是实现祖孙之间通信。(父子之间也可以使用,但是可以使用更简单的props)
祖先组件使用provide(别名,数据)
来提供数据
后代组件使用inject(别名)
来接收数据
provide 和 inject 通常成对一起使用,使一个祖先组件作为其后代组件的依赖注入方,无论这个组件的层级有多深都可以注入成功,只要他们处于同一条组件链上。
这个 provide 选项应当是一个对象或是返回一个对象的函数。这个对象包含了可注入其后代组件的属性。你可以在这个对象中使用 Symbol 类型的值作为 key。
在提供数据的祖先组件中引入provide
API
import {provide } from "vue";
setup() {
let per = reactive({
name: "张三",
age: 21,
});
provide("perData", per);//注入名和注入值
return { ...toRefs(per) };
},
在接收数据的后代组件中引入inject
API
import { inject } from "vue";
打印输出接收到的值,发现也是一个响应式结构。
setup() {
let perRes = inject("perData");
console.log(perRes);
return { ...toRefs(perRes) };
},
检查一个值是否为ref对象
检查一个对象是否由reactive定义
检查一个数据是否是由readonly创建的只读代理
检查一个对象是否有reactive或readonly方法创建的代理
let per = reactive({
name: "张三",
age: 21,
});
let per2 = ref({
name: "张三",
age: 21,
});
let sum = ref(0);
let r = readonly(per);
console.log(isRef(sum)); //true
console.log(isReactive(per)); //true
console.log(isReactive(per2)); //false
console.log(isRef(per2)); //true
console.log(isReadonly(r)); //true,readonly设置per为只读代理
Fragment
虚拟元素中作用:Teleport标签是一种能够将我们的组件HTML结构移动到指定位置的技术
当存在一个嵌套很深的组件关系的时候,对应的某个组件如果是根据v-if
来确定是否显示或隐藏那么就会存在一个问题,即当该组件显示的时候,会撑大原有的组件。这样子是很不好的。虽然可以使用定位解决,但是定位的嵌套关系很难维护。
如上两副图对比就会发现,突然出现的图片会影响其他组件的布局。那么如何控制。这个时候就引入了Teleport
标签
teleport
标签有一个to
属性,该属性内部可以写HTML元素或写选择器,如body,最终就将内部结构放入body中,而非源组件中。
<template>
<div>
<button @click="isShow = true">点击显示</button>
<teleport to="body" v-if="isShow">
<div class="mask">
<div class="dialog">
<h1>我是对话窗口</h1>
<button @click="isShow = false">点击隐藏</button>
</div>
</div>
</teleport>
</div>
</template>
作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验
前提知识,同步加载组件和异步加载组件
两种引入方式均在低速网络中引入
就是平常import
引入的语句。但是这种引入存在一个问题,如果一个组件嵌套多个其他组件或使用到了其他组件。由于采用同步引入的方式,当网络发送堵塞的时候,整个页面是处于一起加载的情况,一旦网络请求成功就一起显示。即当HTML代码执行到JS部分的代码,就会去执行JS部分,由于JS代码会引起HTML代码的阻塞,而JS发送网络堵塞,就这造成了无法成功解析HTML显示。
采用vue提供的apidefineAsyncComponet
异步引入组件。该方法需要传入一个函数作为参数,且参数需要有一个返回值,通常在这里采用按需引入import
引入其他组件。
import { defineAsyncComponent } from "vue";
const Child = defineAsyncComponent(() => import("@/components/Child.vue"));
export default {
name: "App",
components: { Child },
};
这个时候当网络堵塞,就会先执行HTML结构,当渲染到JS部分组件标签的时候就会放入其他队列执行,优先执行完HTML。让用户线看见页面。
但是这方式存在一个页面问题,即先加载是数据先显示给用户,后加载的页面信息会突然出现,造成页面抖动,这个时候就引入了Suspense
组件
Suspense
标签组件有两个插槽:default
和 fallback
。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。
在初始渲染时, 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后, 会进入完成状态,并将展示出默认插槽的内容。
如果在初次渲染时没有遇到异步依赖, 会直接进入完成状态。
既然要使用带名字的插槽:default
和 fallback
,那么就必须使用一个template
标签包裹,并使用vue3的v-slot:指定存放的插槽
<template>
<div class="App">
<h1>我是App组</h1>
<Suspense>
<template v-slot:default>
<Child></Child> //异步加载区域
</template>
<template v-slot:fallback>
<h1>loading处理,稍等片刻。。。</h1>
</template>
</Suspense>
</div>
</template>
```javascript
import { defineAsyncComponent } from "vue";
const Child = defineAsyncComponent(() => import("@/components/Child.vue"));
当使用异步引入组件配合Suspense组件的时候,可以在组件中使用一些异步操作,在上述代码的基础上,在Child组件中添加如下异步操作。可以发现该异步操作可能会导致组件加载的时机。通俗讲就是加载的的时候,会发现加载中字样显示的时候,网络不转圈加载了,但是Child组件没有加载出来,这是由内部异步操作引起的
setup() {
let data = ref(0);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data });
}, 1000);
});
},
代码也可以该成如下
async setup() {
let data = ref(0);
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data });
}, 3000);
});
return await p;
},
注意:在同步引入组件的情况下,不能在setup函数中使用async函数
Vue2 | Vue3 |
---|---|
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 |
2.过渡类名的更改完善
vue2中
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
vue3中
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
config.keyCodes
v-on.native
修饰符父组件
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
子组件
<script>
export default {
emits: ['close']//click不写默认为原生事件,写了就为自定义事件
}
</script>