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
1.1、Vue3带来了什么
1.性能的提升
Vue3与Vue 2相比,在包大小(使用 tree-shaking 时减轻多达 41%)、初始渲染(速度提高多达 55%)、更新(多达 133% 更快)和内存使用(最多减少 54%)。
-
打包大小减少41%
-
初次渲染快55%, 更新渲染快133%
-
内存减少54%
......
2.源码的升级
-
使用Proxy代替defineProperty实现响应式
-
重写虚拟DOM的实现和Tree-Shaking
......
3.拥抱TypeScript
- Vue3可以更好的支持TypeScript
4.新的特性
-
Composition API(组合API)
- setup配置
- ref与reactive
- watch与watchEffect
- provide与inject
- ......
-
新的内置组件
- Fragment
- Teleport
- Suspense
-
其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除keyCode支持作为 v-on 的修饰符
- ......
具体可查看:https://github.com/vuejs/core/releases?q=3.0.0&expanded=true
2、创建vue3工程
2.1、使用 vue-cli 创建工程
从 vue-cli 的 v4.5.0 版本开始,vue-cli 提供了内置选项,可在创建新项目时选择 Vue 3。所以通过 vue-cli 创建 vue3项目需要保证 vue-cli 版本 >= v4.5.0。
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上 vue --version ## 当版本过低时,安装或者升级你的@vue/cli npm install -g @vue/cli
在更新 vue-cli 时,如果遇到以下错误:
则可能是 node.js 版本太低的原因,先更新 node.js 版本,比如升级至 v12.11.0 版本,然后卸载 vue-cli ,再重新安装即可。
创建 vue3 项目,命令如下,当遇到选择 vue3 还是 vue2 时,选择 vue3 即可。
## 创建 vue create vue3_test ## 启动项目 cd vue3_test npm run serve
可以看到运行结果如下,跟 vue2 没有什么差别:
2.2、使用 vite 创建工程
- 什么是vite?—— 新一代前端构建工具。
- 优势如下:
- 开发环境中,无需打包操作,可快速的冷启动。
- 轻量快速的热重载(HMR)。
- 真正的按需编译,不再等待整个应用编译完成。
- 传统构建 与 vite构建对比图
创建工程命令如下:
## 创建工程 npm init vite-app vue3_test_vite ## 进入工程目录 cd vue3_test_vite ## 安装依赖 npm install ## 运行 npm run dev
运行效果如下:
3、组合式API(Composition API)
3.1、composition和options的对比
options API 开发出来的vue应用如左图所示,它的特点是理解容易,因为各个选项都有固定的书写位置,比如响应式数据就写到data选择中,操作方法就写到methods配置项中等,应用大了之后,相信大家都遇到过来回上下找代码的困境
composition API开发的vue应用如右图所示,它的特点是特定功能相关的所有东西都放到一起维护,比如功能A相关的响应式数据,操作数据的方法等放到一起,这样不管应用多大,都可以快读定位到某个功能的所有相关代码,维护方便,设置如果功能复杂,代码量大,我们还可以进行逻辑拆分处理
- 特别注意:
- 选项式api和组合式api俩种风格是并存的关系 并不是非此即彼
- 需要大量的逻辑组合的场景,可以使用compition API进行增强
4、组合式api之setup 组件选项
setup() 函数是vue3中专门新增的方法,可以理解为Composition Api的入口。
setup 函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
使用示例:
<template> <h1>一个人的信息</h1> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> <h3>工作种类:{{job.name}}</h3> <h3>工作薪水:{{job.salary}}</h3> <button @click="changeInfo">修改人的信息</button> </template> <script> import {ref} from 'vue' export default { name: "App", setup(props) { //数据 let sex = "男" // 直接赋值的变量并不具备响应式功能 let name = ref("张三"); //需通过ref定义响应式数据 let age = ref(18); let job = ref({ name: "前端工程师", salary: "30K", }); //方法 function changeInfo() { console.log(sex, name, age) sex = "女" // 修改sex的值页面无法得到实时更新,sex不是响应式的 name.value = '李四' age.value = 48 console.log(job.value); job.value.name = 'UI设计师' job.value.salary = '60K' } //返回一个对象(常用) return { name, age, job, changeInfo, }; }, }; </script>
效果如下:
注意:
- setup() 虽然可以和 vue2 中的一些配置(如data、methods等)共存,但尽量不要与Vue2.x配置一起使用。当 setup() 和 Vue2 中的配置(data、methos、computed...)同时存在时,在vue2 中的配置项可以访问到setup中的属性、方法。但在setup中不能访问到Vue2的配置(data、methos、computed...)。并且如果两者之间有同名的属性值,则页面中优先使用 setup 中的属性值。
- setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
setup 函数是一个新的组件选项,作为组件中组合式API 的起点(入口)。该函数有以下特性:
- setup() 函数是在 props 解析之后,beforeCreate() 执行之前执行的,并且 setup函数只会在组件初始化的时候执行一次。
setup
的调用发生在data
属性、computed
属性、methods
等等属性被解析之前,所以它们无法在setup
中被获取。- 在 setup() 中你应该避免使用
this
,因为它不会找到组件实例,在 setup() 中 this 指向的是 undefined。
4.1、setup() 函数的参数
setup() 函数有两个参数:props 和 context。
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象。context 是一个普通的 JavaScript 对象,它具有以下三个属性:
- attrs:值为对象,包含了组件外部传递过来但没有在 props 配置中声明的属性,相当于 vue2 中的
this.$attrs
。 - slots:组件收到的插槽内容,相当于vue2 中的
this.$slots
。 - emit:用于触发自定义事件的函数,相当于vue2 中的
this.$emit
。
示例:
子组件 Demo 定义如下:
<template> <button @click="test">测试触发一下Demo组件的Hello事件</button> </template> <script> export default { name: 'Demo', props:['msg1'], emits:['hello'], setup(props,context){ console.log('props:',props) console.log('context:',context) console.log('context.attrs:',context.attrs) //相当与Vue2中的$attrs console.log('context.emit:',context.emit) //触发自定义事件的。 console.log('context.slots:',context.slots) //插槽 //方法 function test(){ context.emit('hello',666) } //返回一个对象(常用) return { test } } } </script>
父组件如下:
<template> <Demo @hello="showHelloMsg" msg1="信息1" msg2="信息2" > <template v-slot:slot01> <span>插槽1</span> </template> <template v-slot:slot02> <span>插槽2</span> </template> </Demo> </template> <script> import Demo from "./components/Demo"; export default { name: "App", components: { Demo }, setup() { function showHelloMsg(value) { alert(`你好啊,你触发了hello事件,我收到的参数是:${value}!`); } return { showHelloMsg, }; }, }; </script>
执行完成后,可以看到子组件的输出如下:
可以看到,子组件的 setup() 函数中,输出的参数 props 只包含了在子组件中已通过 props 配置项接收了的属性值,而 context.attrs 只包含了未通过 props 配置接收的属性值;context.emit 相当于 this.$emit,可用于触发给子组件绑定了的自定义事件;context.slots 可获取插槽内容。
5、组合式api之ref() 函数(返回 RefImpl 对象)
ref() 函数接受一个简单类型或者复杂类型的传入,并返回一个响应式且可变的 ref 实例对象(reference对象,简称ref对象)。当 ref 接收对象(或数组)类型数据时,实际上它的内部会自动通过 reactive 来监听这些对象,实际上相当于使用了 reactive() 来监听这些对象。
使用步骤:
- 从vue框架中导入ref函数
- 在setup函数中调用ref函数并传入数据(简单类型或者复杂类型),把ref函数的返回值以对象的形式返回
- 注意:在setup函数中使用ref结果,需要通过.value 访问,模板中使用不需要加.value
示例如下:
<template> <div>{{ money }}</div> </template> <script> import { ref } from 'vue' export default { setup() { let money = ref(100) let obj = ref({ name: 'wen' }) console.log(11, money) console.log(22, obj) return { money } } } </script>
输出如下:
ref() 函数接收的数据可以是基本类型和对象类型。基本类型数据的响应式依然是靠Object.defineProperty()
的get
与set
完成的;而对象类型的数据在内部通过Vue3.0中的一个新函数 reactive
函数来实现响应式。
使用 ref() 函数时,修改返回的响应式对象需要通过 value 属性值进行修改,如:xxx.value = 'xxx'。但是在页面模板中读取数据时不需要 value,直接使用即可,如:<div>{{xxx}}</div>。
6、组合式api之reactive() 函数(返回 proxy 对象)
reactive 函数接收一个普通的对象传入,把对象数据转化为响应式对象并返回。
使用步骤:
- 从vue框架中导入reactive函数
- 在setup函数中调用reactive函数并将对象数据传入,把reactive函数调用完毕之后的返回值以对象的形式返回
示例:
<template> <div>{{ state.name }}</div> <div>{{ state.age }}</div> <button @click="state.name = 'pink'">改值</button> <button @click="changeInfo">修改人的信息</button> </template> <script> import { reactive } from 'vue' export default { setup () { const state = reactive({ name: 'cp', age: 18 })
console.log(11, state)
function changeInfo(){ state.name = '李四' state.age = 48 } return { state, changeInfo } } } </script>
输出如下:
reactive 不能接收基本类型作为参数。reactive 返回一个代理对象(Proxy的实例对象,简称proxy对象),reactive定义的响应式数据是“深层次的”,内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
6.1、reactive 和 ref 的对比
- 从定义数据角度对比:
- ref用来定义:基本类型数据、对象类型。(ref 定义对象(或数组)类型数据时,它的内部会自动通过
reactive
转为代理对象。) - reactive用来定义:对象(或数组)类型数据。
- ref用来定义:基本类型数据、对象类型。(ref 定义对象(或数组)类型数据时,它的内部会自动通过
- 从原理角度对比:
- ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 - reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
- ref通过
- 从使用角度对比:
- ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive定义的数据:操作数据与读取数据:均不需要
.value
。
- ref定义的数据:操作数据需要
7、组合式api之 computed() 函数
computed() 函数与Vue2.x中 computed 配置项功能基本一致。computed() 函数也可以像 computed 配置项一样设置它的 setter。
使用示例:
<template> <span>全名:{{person.fullName}}</span> 全名:<input type="text" v-model="person.fullName"> </template> <script> import {reactive,computed} from 'vue' export default { name: 'Demo', 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] } })
return { person } } } </script>
8、组合式api之 watch() 函数
watch() 函数与Vue2.x中 watch 配置项功能基本一致。
使用方法:首先需在 vue 中引入 watch,然后在 setup 中直接通过 watch() 函数监听响应式数据。
语法如下:
// 第一个参数是监视的属性名,可以监听单个属性,也可以一次性监听多个属性,此时该参数为数组。第二个参数为监视回调。第三个参数可选,可配置监视的一些配置,比如immediate、deep,类似于vue2的使用 watch(监视的属性名, ()=> { ... }, {}),
8.1、watch() 监听 ref 实例对象
当 watch 监听 ref 实例对象 refImpl 时,实际上监听的是实例对象的 value 属性值。
使用示例如下:
setup(){ //数据 let sum = ref(0) let msg = ref('你好啊')//监视ref所定义的一个响应式数据 /* watch(sum,(newValue,oldValue)=>{ console.log('sum变了',newValue,oldValue) },{immediate:true}) */ //可一次性监视ref所定义的多个响应式数据。当然,也可以直接调用多个watch函数也行 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变了',newValue,oldValue) },{immediate:true}) //返回一个对象(常用) return { sum, msg } }
当 ref() 函数参数为对象时,此时该实例对象的 value 值为一个 proxy 代理对象,所以如果此时监听该实例对象时,实际上只有当整个实例对象发生改变时监听回调函数才会触发。如下:
<template> <h2>年龄:{{person.age}}</h2> <!-- 此时点击按钮并不会触发监听的回调函数 --> <button @click="person.age++">修改person.age的值</button> </template> <script> import {reactive, ref, watch} from 'vue' export default { name: 'Demo', props:['msg1'], emits:['hello'], setup(props,context){ let person = ref({ name: 'aa', age: 12 }) watch(person, (newValue,oldValue)=>{ console.log('person发生了改变', newValue, oldValue) }) return { person } } } </script>
此时点击按钮修改 person.age 的值并不会触发监听的回调函数,只有当给 person 重新赋值时才会触发监听的回调函数。
如果我们想要监听 person 下的属性值的改变时,此时可以将 watch 函数的参数改为 person.value,即监听 proxy 代理对象,或者在 watch 函数的第三个参数添加 deep 参数。如下:
<template> <h2>年龄:{{person.age}}</h2> <!-- 此时点击按钮并不会触发监听的回调函数 --> <button @click="person.age++">修改person.age的值</button> </template> <script> import {reactive, ref, watch} from 'vue' export default { name: 'Demo', props:['msg1'], emits:['hello'], setup(props,context){ let person = ref({ name: 'aa', age: 12 }) //直接监听 value 值 watch(person.value, (newValue,oldValue)=>{ console.log('person发生了改变', newValue, oldValue) }) // 添加deep配置 watch(person, (newValue,oldValue)=>{ console.log('person发生了改变', newValue, oldValue) },{ deep: true }) return { person } } } </script>
8.2、watch 监听 reactive 响应式对象
通过 watch() 函数可以监听 reactive() 返回的 proxy 响应式对象。
需要注意,如果监听的是一个对象的话,监听的回调函数无法正确获取到 oldvalue 的值,输出 oldvalue 会得到跟 newvalue 相同的值。
- 如果监听的是 reactive 返回的整个对象的话,此时会默认开启深度监听,并且无法关闭深度监听,另外还有点小问题,就是此时无法正确获取 oldvalue 的值,输出 oldvalue 会得到跟 newvalue 相同的值。
- 需要注意的是,如果监听的是 ref() 函数以对象作为参数所返回的响应式数据,也同样有上述问题。
- 如果想要监听 reactive 返回的对象的某个属性值的话,此时 watch() 函数的第一个参数应该是一个函数,并且返回值为需要监听的对象的属性。但此时并不会自动开启深度监听,也就是说,假如我们监听的这个属性值是对象的话,修改这个对象的属性是不会触发监听回调函数的。
- 如果监听的这个 reactive 返回的对象的属性是字符串的话,则此时能正确获取到 oldvalue 的值
- 如果监听的这个 reactive 返回的对象的属性是对象的话,此时就无法正确获取到 oldvalue 的值
setup(){ //数据 let sum = ref(0) let msg = ref('你好啊') let person = reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } } }) /* 监视reactive所定义的一个响应式数据的全部属性 1.注意:此处无法正确的获取oldValue,输出的oldvalue的值总是等于newvalue的值 2.注意:此时会强制开启深度监视(并且无法通过deep配置关闭深度监听) */ watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) },{deep:false}) //此处的deep配置无效 //可以直接监视reactive所定义的一个响应式数据中的某个属性。此时可以正确获取oldvalue值的情况 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) }) //特殊情况,监听的属性值是一个对象,此时也无法获取oldvalue的值 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效 */ //返回一个对象(常用) return { sum, msg, person } }
9、组合式api之 watchEffect() 函数
watchEffect() 函数不用指明监视哪个属性,监视的回调中用到哪写属性,那就会监视哪些属性,当这些属性值发生改变时,回调函数就会执行。
watchEffect有点像computed,但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值,而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
示例如下:
export default { name: 'Demo', setup(){ //数据 let sum = ref(0) let msg = ref('你好啊') let person = reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } } }) watchEffect(()=>{ const x1 = sum.value const x2 = person.job.j1.salary console.log('watchEffect所指定的回调执行了') }) //返回一个对象(常用) return { sum, msg, person } } }
则只要 sum 或者 person.job.j1.salary 的值发生改变,watchEffect() 函数就会执行。
10、vue3的生命周期
Vue3 和 Vue2 中的生命周期钩子的工作方式类似,名称也差不多,只是有两个钩子函数的名称改了而已,实际上他们的触发时机都是一样的。
vue3 的生命周期如下:
- beforeCreate
- created
- beforeMount
- mounted
- beforeupdate
- updated
-
beforeUnmount(相当于vue2中的beforeDestroy)
-
unmounted(相当于vue2中的destroyed)
使用示例:
export default { name: 'Demo', setup(){ console.log('---setup---') //数据 let sum = ref(0) //返回一个对象(常用) return {sum} }, //通过配置项的形式使用生命周期钩子 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---') } }
10.1、组合式api形式的生命周期钩子
Vue3.0 中提供了 Composition API 形式的生命周期钩子,你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
beforeCreate
===>setup()
created
=====>setup()
beforeMount
===>onBeforeMount
mounted
=====>onMounted
beforeUpdate
===>onBeforeUpdate
updated
====>onUpdated
beforeUnmount
==>onBeforeUnmount
unmounted
=====>onUnmounted
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义这两个钩子。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
使用示例:
import {ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue' export default { name: 'Demo', setup(){ console.log('---setup---') //数据 let sum = ref(0) //通过组合式API的形式去使用生命周期钩子 onBeforeMount(()=>{ console.log('---onBeforeMount---') }) onMounted(()=>{ console.log('---onMounted---') }) onBeforeUpdate(()=>{ console.log('---onBeforeUpdate---') }) onUpdated(()=>{ console.log('---onUpdated---') }) onBeforeUnmount(()=>{ console.log('---onBeforeUnmount---') }) onUnmounted(()=>{ console.log('---onUnmounted---') }) //返回一个对象(常用) return {sum} } }
实际上,组合式 api 形式的生命周期钩子和配置项形式的钩子可以同时存在,当同时存在时,组合式 api 形式的执行要先于对应的相同的配置项的钩子。但我们不建议同时使用组合式 api 和配置项的钩子,没什么必要。
11、Composition API 的优势
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。使用 Composition API 我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。