2020年9月18日,Vue.js发布了3.0版本,代号:One Piece(海贼王)
耗时两年多,2600+次提交、30+个RFC、600+次PR、99位贡献者
Vue3带来了什么:
1、性能上的提升
2、源码的升级
3、拥抱TypeScript
4、新特性
(1)Composition API(组合API)
(2)新的内置组件
(3)其他改变
官方文档: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
官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite
vite官网:https://vitejs.cn
什么是vite?——新一代的前端构建工具
优势如下:
1.开发环境中,无需打包操作,可快速冷启动
2.轻量快速的热重载(HMR)
3.真正的按需编译,不再等待整个应用编译完成
// 创建工程
npm init vite-app <project-name>
// 进入工程目录
cd <project-name>
// 安装依赖
npm install
// 运行
npm run dev
之前我们在vue2中都是多个配置项组合在一起(选项式API:data、method那些),在vue3中,换了一种方式,那就是Composition API(组合式API),而setup则是组合式API表演的舞台,组件中所有的数据和方法都写在setup函数中。
这个setup函数其实有两种返回值:
返回一个对象
返回一个渲染函数
用的最多的就是返回一个对象,把用到的所有属性、方法都返回出去,方便在模板中使用(当然,现在有了setup语法糖,后面再说)
<template>
<h1>个人信息h1>
<h2>姓名:{{ name }}h2>
<h2>年龄:{{ age }}h2>
<button @click="sayHello">打招呼button>
template>
<script>
import { h } from "vue";
export default {
name: "App",
// 此处只是测试一下setup,暂时不考虑响应式的问题
setup() {
// 数据
let name = "potato";
let age = 18;
// 方法
function sayHello() {
alert(`我叫${name},我${age}岁了`);
}
// 返回一个对象
return {
name,
age,
sayHello,
};
// 返回一个渲染函数
// return () => {return h('h1','哈哈哈哈')};
},
};
script>
注意:不要把setup和vue2中的选项式API混用,因为setup中访问不到vue2中的属性方法(但2可访问3),如果混用的话,有重名setup优先。
还有,setup前不要加async,因为返回值会变成promise,而不是我们return的那些属性和方法
还记得vue2的ref属性吗,vue2的ref类似于id。复习Vue2的ref属性
而Vue3的ref是一个函数。
作用:定义响应式的数据。
例:点击按钮修改信息
<template>
<h1>个人信息h1>
<h2>姓名:{{ name }}h2>
<h2>年龄:{{ age }}h2>
<button @click="changeInfo">修改信息button>
template>
<script>
import { ref } from "vue";
export default {
name: "App",
setup() {
// 数据
let name = ref("potato");
let age = ref(18);
// 方法
function changeInfo() {
// 直接这样写的话是不行的,因为ref把数据包装成了对象,对其操作时要加上‘.value’
// name= "李四";
// age= 13;
// console.log(name, age);
name.value = "李四";
age.value = 13;
}
// 返回一个对象
return {
name,
age,
changeInfo,
};
},
};
script>
const xxx = ref(initValue)
创建了一个包含响应式数据的引用对象(RefImp对象)
在js中操作数据:count.value
在模板中读取数据:
(这里会自动读取value属性)
其实ref可以接受的数据不只是基本数据类型,复杂数据类型也可以,只不过比较麻烦,操作时都要.value
<template>
<h1>个人信息h1>
<h3>职业:{{ job.type }}h3>
<h3>工作地点:{{ job.address }}h3>
<button @click="changeInfo">修改信息button>
template>
<script>
import { ref } from "vue";
export default {
name: "App",
setup() {
// 数据
let job = ref({
type: "前端工程师",
address: "广州",
});
// 方法
function changeInfo() {;
job.value.type = "后端工程师";
job.value.address = "深圳";
}
// 返回一个对象
return {
job,
changeInfo,
};
},
};
script>
基本数据类型和对象数据类型实现响应式的方式不同,前者依然是Object.defineProperty()的get与set
,后者则在内部求助了reactive
函数
作用:定义一个对象类型的响应式数据(基本类型不要用reactive,要用ref)
<script>
import { ref, reactive } from "vue";
export default {
name: "App",
setup() {
// 数据
let job = reactive({
type: "前端工程师",
address: "广州",
});
// 数组也可以
let hobby = reactive(["吃饭", "睡觉", "打豆豆"]);
// 方法
function changeInfo() {
job.type = "后端工程师";
job.address = "深圳";
hobby[2] = "学习";
}
// 返回一个对象
return {
job,
changeInfo,
hobby,
};
},
};
</script>
const 代理对象 = reactive(源对象)
接收一个对象或数组,返回一个代理对象(Proxy对象)
这里要注意,只有改变job中的某个属性,才可以实现响应式,如果直接改job,会报警告告诉你job是只读(read-only)的。如果要改job,需要Object.assign(job, newObj);或者job= { …job, …newProperties };
在了解Vue3响应式原理之前,咱先来复习下vue2响应式。
我们知道vue2中是通过Object.defineProperty()
对对象类型数据进行劫持并添加gette
r和setter
来监视数据的,对于数组是重写了更新数组的方法。复习vue2监视数据的原理
vue2监视数据存在两个问题:
这两个问题可以通过$set
、$delete
、splice(数组变更方法)
解决
在vue3中,解决了vue2中的两个问题。
<button @click="addSex">添加一个sex属性button>
<button @click="deleteName">删除一个name属性button>
function addSex() {
// 如果是vue2,要这样写this.$set(this.person, 'sex', '女');
person.sex = "女";
}
function deleteName() {
// 如果是vue2,要这样写this.$delete(this.person, 'name');
delete person.name;
}
下面是模拟实现vue3中响应式的原理:
<script>
let person = {
name: "potato",
age: 18,
};
// 模拟Vue3实现响应式
const p = new Proxy(person, {
// 有人在读取p的某个属性时调用
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`);
return target[propName];
},
// 有人在修改p的某个属性、或给p追加某个属性时调用
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性,我要去更新界面了`);
target[propName] = value;
},
// 有人在删除p的某个属性时调用
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性,我要去更新界面了`);
return delete target[propName];
},
});
</script>
再严谨一些,用Reflect(反射)
来进行增删改查,这也是框架的做法,其实本质上没什么太大的区别,只是Reflect
可以更好捕捉错误,避免一些报错吧,不然还要用try-catch
去捕获。这里不用太纠结,其实就是为了让框架更加的友好(少一些报错)
Proxy(代理)
+Reflect(反射)
<script>
let person = {
name: "potato",
age: 18,
};
// 模拟Vue3实现响应式
const p = new Proxy(person, {
// 有人在读取p的某个属性时调用
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`);
return Reflect.get(target, propName);
},
// 有人在修改p的某个属性、或给p追加某个属性时调用
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性,我要去更新界面了`);
Reflate.set(target, propName, value);
},
// 有人在删除p的某个属性时调用
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性,我要去更新界面了`);
return Reflate.deleteProperty(target, propName);
},
});
</script>
对于ref
和reactive
是不一样的
ref简单类型是通过:Object.defineProperty()的get与set,当然啊,ref定义的复杂类型是通过:reactive的Proxy
reactive是通过Proxy来实现响应式的(上文提到了),并通过Reflect来操作源数据
不管怎么样,总结来说,vue3中新增的就是对于复杂数据类型通过Proxy实现响应式,也就是两个点:
new Proxy(person, {
// 拦截读取属性值
get(target, propName) {
return Reflect.get(target, propName);
},
// 拦截设置属性值或添加属性值
set(target, propName, value) {
Reflate.set(target, propName, value);
},
// 拦截删除属性
deleteProperty(target, propName) {
return Reflate.deleteProperty(target, propName);
},
});
(1)定义数据类别不同
ref
用于定义基本类型数据reactive
用于定义对象或数组类型数据ref
也可以定义对象或数组类型数据,它内部会自动通过reactive
转为代理对象(2)原理不同
Object.defineProperty()
的get
与set
,当然啊,复杂类型是通过reactive的ProxyProxy
来实现响应式的(上文提到了),并通过Reflect
来操作源数据(3)使用方式不同
ref
定义的数据,操作时需要.value,模板读取不需要.valuereactive
定义的数据,操作和读取都不需要.valuesetup
函数的执行时机是beforeCreate之前
,也就是所有生命周期的最前面,此时this是undefined,也就是说在setup中是绝对不能通过this访问组件实例的。
注意这里的beforeCreate是写在setup外面的,如果在setup里面,是没有beforeCreate和created这两个钩子的,因为setup的执行时机就相当于这两个钩子(setup会先于所有的钩子执行,把想在这两个钩子里写的代码写到steup中最前面就行了)
setup接收两个参数:(props,context)
props
是一个对象,包含:组件外部传递过来,且组件内部声明接收了的属性。context
是一个对象,包含三个属性,分别是attrs、slots、emit
attrs
相当于this.$attrs
,值为对象,包含组件外部传递过来,但没有在props
配置中声明的属性slots
相当于this.$slots
,包含收到的插槽的内容。emit
相当于this.$emit
,用来触发组件的自定义事件。props: ['name', 'age']
setup(props, context) {
console.log(props) // Proxy{name:'potato',age:18}:组件外部传递过来,且组件内部声明接收了的属性。
console.log(context.attrs)//相当于this.$attrs
console.log(context.slots)相当于this.$slots
console.log(context.emit)//相当于this.$emit
}
和vue2中功能是一样的,只不过变成了一个函数,而且要手动引入。默认写法是传一个回调
如果我们只使用简写的话,当我们去修改计算属性值的时候,控制台会有警告,告诉我们这个属性值是只读的。
这是因为计算属性默认是只读的,当它所依赖的值改变的时候,它自己会变。如果要想改计算属性,需要用下面的完整写法,传一个对象,里面有getter
和setter
,setter
中参数是修改后的新值
<template>
<h1>个人信息h1>
姓:<input type="text" v-model="person.firstName" />
<br />
名:<input type="text" v-model="person.lastName" />
<br />
全名:<input type="text" v-model="person.fullName" />
<br />
<span>全名:{{ person.fullName }}span>
template>
<script>
import { computed, reactive } from "vue";
export default {
setup() {
let person = reactive({
firstName: "张",
lastName: "三",
});
// 计算属性-简写(没有考虑计算属性被修改的情况)
// person.fullName = computed(function () {
// 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];
},
});
return {
person,
};
},
};
script>
其实watch在vue3中和vue2中功能是一样的,就是写法不一样。
vue2的监听函数写法点此复习vue2监听函数
先准备一些数据吧:
import { ref, reactive } from 'vue'
const sum = ref(0)
const msg = ref('hello')
const person = reactive({
name: 'potato'
age: 18
job: {
type: 'code'
salary: 10
}
})
watch(sum, (newVal, oldVal) => {
console.log("sum的值变了", newVal, oldVal);
});
watch([sum, msg], (newVal, oldVal) => {
console.log("sum或msg变化了", newVal, oldVal);//new和old也是监听值构成的数组
});
reactive
定义的数据,监视时回调中无法获得oldValue
!oldValue和newValue一样reactive
定义的数据,默认开启的deep:true
,且deep
不能改成false
watch(person, (newVal, oldVal) => {
console.log("person变化了");
},{immediate:true,deep:false});
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
如果我们把person换成是一个ref定义的呢?那么监视的时候写不写.value
?
import { ref, reactive } from 'vue'
const sum = ref(0)
const msg = ref('hello')
const person = ref({
name: 'dantin'
age: 18
job: {
type: 'code'
salary: 10
}
})
答案是:要写的,因为ref
对于复杂数据类型,内部是借助reactive
的Proxy
实现响应式的,所以这么写的话就相当于是写了一个reactive
定义的响应式数据,在监视时也就具有了对应的坑(见上文情况三)
watch(person.value,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不奏效
除此之外还有一种办法,那就是深度监视person(它是一个RefImpl对象
),这样就能监视到其中的value及更深层的变化。这是因为如果直接监视person不读取.value
,那么监视的是RefImpl
对象,只有其中value的地址变的时候才能监视到,value里面的东西变化是监视不到的,所以要开deep
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:true})
那为什么简单数据类型不需要.value
呢?其实和上面的情况四是一样的,如果简单数据类型直接.value
,那么监视的就是一个写死的值。不.value
的话,监视的是一个响应式的RefImpl
对象,当里面value变化的时候是可以监视到的
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
})
如果非要.value
,请使用箭头函数动态读取,每次sum变化都会执行回调读取最新的值
watch(() => sum.value,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
})
watch
的套路是:既要指明监视的属性,也要指明监视的回调。watchEffect
的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。watchEffect有点像computed,但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
//watchEffect的回调一上来会先执行一次
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(() => {
const x1 = sum.value;
const x2 = person.job.salary;
console.log("watchEffect所指定的回调执行了");
});