# Vue3 的正确打开方式
Vue3 已经出道了 3.2 版本了,但是目前 Vue3 的普及程度还是远不如 Vue2;但是 Vue3 的学习却是已经刻不容缓了;Vue2 的项目能否直接升级 Vue3 呢?那么 Vue2 到 Vue3 消失了那些 API?Vue3 新增或增强的 API 呢?
# Vue2 的项目能否直接升级 Vue3 呢
Vue2 的项目不能直接升级 Vue3;因为 Vue3 对 Vue2 并不是全兼容的;但是如果你把 Vue3 当成 Vue2 来使用也是没有问题的(只是用来做一些简单数据渲染,状态绑定);
# 消失的 API
# 消失的生命周期
- beforeDestroy --> beforeUnmount
- destroyed --> unmounted
从官方文档上看,这两个钩子有啥区别,不知道为什么要换名字;
既然没有了destroyed,那么自然在实例中就不存在$destroy方法了;以后再也不能愉快的手动销毁组件了;
# 消失的$listeners
不得不说$attrs, $listener的出现是 Vue2 的以一大进步,在早期开发中还没有$attrs, $listener的时候,只要做组件的封装不得不吐槽 Vue2 的呆板,跟 react 相比,只能说 react 的全属性继承,全事件透传是真香;
Vue3 中$listener已经被遗弃;但是Vue3并没有放弃全事件透传的能力;反而更加向react靠拢,书写起来更方便,更简单;将$listeners 的与$attrs合并;以后再不用组件上使用v-on="$listeners"了;来看一下代码的区别;
Vue2 中:
<!-- 父组件 -->
<template>
<child :placeholder"请输入" :max-length="10" @change="changeHandler"></child>
</template>
<!-- 子组件 -->
<template>
<div >
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
</div>
</template>
Vue3
<template>
<child :placeholder"请输入" :max-length="10" @change="changeHandler"></child>
</template>
<!-- 子组件 -->
<template>
<div >
<el-input v-bind="$attrs"></el-input>
</div>
</template>
Vue3 当需要透传的属性是动态的时候:
<template>
<child :attrs="attrs"> </child>
</template>
<script>
export default {
data() {
return {
attrs: {
placeholde: '请输入',
maxLength: 10,
onChange: this.changeHandler
}
}
},
methods: {
changeHandler() {}
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<el-input v-bind="attrs"></el-input>
</div>
</template>
<script>
export default {
props: {
attrs: {
type: Object,
default: () => ({})
}
}
}
</script>
重点
当事件作为属性透传的时候一定,必须是以 on 开头,接事件名称
# 消失的 Vue.prototype
import { createApp } from 'vue'
const app = createApp()
Vue3 中,不再支持在 Vue.prototype 上绑定全局属性;
- Vue3 中的 Vue 不再是一个函数,而是一个对象
- createApp 函数返回的是一个 VueComponent 对象, 对象不存在 prototype 属性, 所以也不能在 createApp函数返回的对象的原型链上绑定全局属性
- Vue 组件中的this对象任然保留 Vue 自定义全局属性,例如($attrs, $nextTick, $set......)
Vue3 中提供了一个全局对象配置 app.config.globalProperties,需要挂载到全局的属性可以放在这个全局对象上;挂载到全局对象上的属性可以再组件内部通过this访问;
提示
setup函数中不存在this对象, 在 setup 中想要获取实例,请使用 getCurrentInstance 方法,该方法返回组件实例;
# 消失的实例属性
- $children
- $scopedSlots
- $isServer
- $listeners
# 消失的特殊指令
- slot
- slot-scope
- scope
总结一句话就是 Vue3 废除作用域插槽,具名插槽;仅支持 v-slot; v-slot 仅限用于 template 组件,且组件仅能只有 v-slot 这一个属性;
# 消失的 observable
observable最早出现在 2.6 的版本中;主要用于做全局响应式数据的存储,在普通的项目中,如果全局响应式的数据不是很多的情况是可以不必要使用 vuex, 用 observable 即可;用法跟 vuex 差不多,简单易用;
在 Vue3 中,Vue3 提供了更为强大的相应 API reactive, 用法跟 observable 一样;
// sotre.js
import { reactive } from 'vue'
export const state = reactive({
username: 'town',
token: 'TK23423423'
})
export function setName(payload) {
state.username = payload
}
// 组建中使用store
import { state, setName } from './store.js'
export default {
computed: {
username: () => state.username
},
methods: {
setUserInfo() {
setName('town2021')
}
}
}
# 消失的过渡 class
.v-enter-->.v-enter-from.v-leave-->.v-leave-from重点
transiton,transition-group可以作为根节点;transition-group不再默认渲染根元素;
# 消失的事件 API
- $on
- $once
- $off
提示
原生事件修饰符.native被移除
# 消失的@hook
@hook是一个组件生命周期监听器,该属性的出现主要是为了方便在父组件,更优雅的监听子组件的生命周期钩子的触发,当然也可以用于在组件中监听自身生命周期钩子的触发;
Vue3 中不再支持@hook, 而是提供了能力更为强大的@vnode-; 该属性不仅可以作用于组件上,也可作用于 HTML 元素上;但是遗憾的是因为没有了$on和$once,@vnode-估计只能作用于 template 中,无法在组件生命周期钩子以及 methods 中使用;
# 消失的 filter
filter在 Vue3 中已经被移除,无论是局部 filter 还是全局 filter 都不支持;局部建议使用计算属性或者方法来替换,全局兼用将 filter 方法挂载到app.config.globalProperties上;
# 消失的 productionTip
对于 ES 模块构建,由于它们是与 bundler 一起使用的,而且在大多数情况下,CLI 或样板已经正确地配置了生产环境,所以本技巧将不再出现
# 消失的 extend
组件继承在 Vue3 中不被建议,Vue3 建议使用组合式 API 来替代继承和 mixin; 如果一定要使用基础,那么官方提供了extends属性;
如果使用 extend 来创建组件,Vue3 中将使用creatApp来替代;
# 消失的 config.keyCodes
Vue2 中可以通过config.keyCodes来配置键盘的按键事件;从KeyboardEvent.keyCode has been deprecated (opens new window) 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称;
提示
Vue3 不再支持使用数字作为v-on修饰符
# 消失的 propsData
propsData 选项在 Vue2 中用于创建 vue 实例的过程中传入 prop,在 Vue3 中被移除;如果想在创建实例的时候传入 prop,需在createApp函数中出入第二参数;
# 消失的内联模板 inline-template
什么是 inline-template:以便将自定义组件内部内容用作模板,而不是将其作为分发内容;
基本用不上,可以忽略,使用该属性的人基本上是闲得无聊的;
# 函数式组件的变化
- 函数式组件不再必须
functional属性,可以是一个纯函数 - 函数式组件接收两个参数
propscontext;(context = {slots, attrs, emit}),跟setup函数一样; - 需要显式的引入 h 函数(
import { h } from 'vue'); - 函数式组件依然支持 Vue 的写法,只不过 render 函数发生了一点点的变化
Vue3 中使用函数式组件的两种方法
// 纯函数
const createDynamicCom = (props, { slots, attrs, emit }) => {
return h('div', attrs, slots)
}
// functional声明
import { h } from 'vue'
export default {
functional: true,
// render不再接收 h函数
render() {
return h('div', this.attrs, this.slots)
}
}
render 函数 接收 6 个参数,但是仅第一个参数有使用价值,且第一个参数的值安全等于 this 对象;所以一般我们直接使用 this 就行;一起来看看 this 对象有哪些属性;
{
$: Object // app实例
$attrs: Proxy // 组件的attribute
$data: Object // 当前组件的data数据
$el: HTMLELement // 当前组件的dom实例
$emit: function // 触发emit时间的函数
$forceUpdate: function // 强制更新组件函数
$nextTick:function
$options: Object // 自身组件的对象数据,
$parent: Proxy // 父组件的引用对象,
$props: Proxy // 显式在组件的props属性中生命的props对象
$refs: Proxy // 当前组件的ref
$root: Proxy // vue实例根节点
$slots: Proxy // 当前组件的插槽
$watch: function // 监听函数
}
重点
$parent 不再是当前函数式组件的父级组件,而是包裹当前函数式组件的 dom 或者自定义组件,这是跟 Vue2 不同的地方, 使用的时候请谨慎;
# 渲染函数 h 的变化
h函数不再能够根据组件名来渲染全局注册的组件;仅支持渲染 dom 标签,以及被加载的组件;
Vue2 中使用 h 函数渲染全局组件
export default {
render() {
return h('el-input')
}
}
Vue3 中使用 h 函数渲染全局组件
import { resolveComponent } from 'vue'
const ELInput = resolveComponent('el-input')
export default {
render() {
return h(ELInput)
}
}
Vue3 中使用 h 函数渲染非全局组件
import { ELinput } from 'element-ui'
export default {
components: {
ELinput
},
render() {
return h(ELInput)
}
}
# 新增或增强的 API
# 增强的 ref
this.$refs[ref] 将返回一个引用数组;
Vue3 中
<div v-for="item in list" :ref="setItemRef"></div>
export default {
data() {
return {
itemRefs: []
}
},
methods: {
setItemRef(el) {
if (el) {
this.itemRefs.push(el)
}
}
},
beforeUpdate() {
this.itemRefs = []
},
updated() {
console.log(this.itemRefs)
}
}
# 新增 defineAsyncComponent
创建一个只有在需要时才会加载的异步组件; 可以接受一个返回 Promise 的工厂函数。Promise 的 resolve 回调应该在服务端返回组件定义后被调用。你也可以调用 reject(reason) 来表示加载失败;
如果仅仅是这样好像跟() => import()没什么区别;
来看一下比较高阶的玩法
import { defineAsyncComponent } from 'vue'
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
# 自定义指令的变化
指令生命周期的变化
| Vue2 | Vue3 | 备注 |
|---|---|---|
| created | 新增 | |
| bind | beforeMount | |
| inserted | mounted | |
| beforeUpdate | 新的!这是在元素本身更新之前调用的,很像组件生命周期钩子 | |
| update | 移除!请改用 updated | |
| componentUpdated | updated | 此处判断组件更新调用十分频繁,如需使用此钩子,一定要写明确的触发的判断条件 |
| beforeUnmount | 新增!与组件生命周期钩子类似,它将在卸载元素之前调用。 | |
| unbind | unmounted |
访问组件实例
// vue2中
bind(el, binding, vnode) {
const vm = vnode.context
}
// vue3中
mounted(el, binding, vnode) {
const vm = binding.instance
}
# data 选项的变化
如果你不是用mixin或者extend,那么 mixin 或 extend 的多个
这简单的一句话暴露了一个非常重大的问题;不了解的话很有可能就踩坑了;
// index.mixin.js
export default {
data () {
return {
userInfo: {
age: 18
}
}
}
}
// 组件中
import IndexMixin from './index.mixin.js'
export default {
mixins: [IndexMixin],
data () {
return {
userInfo: {
name: 'jack',
age: 20
}
}
}
}
// Vue2合并后的结果
data () {
return {
userInfo: {
name: 'jack',
age: 18
}
}
}
// vue3合并后的结果
data () {
return {
userInfo: {
age: 18
}
}
}
# 新增 emits 选项
emits 可以是简单的数组,也可以是对象,后者允许配置事件验证;个人感觉在实际开发中并没有太大的用武之地;因为在向父组件触发事件的时候一般是手动,且目的,数据十分明确的状态下的;但是还是来一起看看如何配置验证函数;
export default {
emits: {
click: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
}
验证函数接收当前组件传递给父组件的参数作为参数;验证函数应返回布尔值,以表示事件参数是否有效;
# 新增 fragments
组件支持多根节点,Vue2 中一个组件只能拥有一个根节点;
# key 属性的非必需性
对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。
template v-for 的 key 应该设置在 template 标签上 (而不是设置在它的子节点上),Vue2 中template不允许使用 key。
# 优化版的$mount
挂载的内容将作为挂载容器的innerHTML; 而非整个替换挂载容器;
# 强化版的 v-model
v-model 不再特指 value,也不再特定使用 update 来触发;
- 现在可以在同一个组件上使用多个 v-model 进行双向绑定
- 现在可以自定义 v-model 修饰符 (不建议使用,使用起来会让你的代码变得更引用,可读性更低;)
# v-if 和 v-for 的优先级调整
Vue3 中 v-if 的优先级将高于 v-for; 真算是 Vue3 的一点点性能优化;但是还是不建议使用模板来做渲染逻辑处理,推荐使用计算属性来处理那些数据数据是否该被渲染;
# 增强的 v-bind
Vue2 中直接声明的 attribute 属性优先级高于 v-bind 中定义的属性;Vue3 中将会将直接生命的 attribute 与 v-bind 中的属性合并;优先级取决于直接声明的 attribute 是书写在 v-bind 的前面还是后面;也就是优先级由顺序决定,越往右优先级越高;
# 新增响应式 API
reactivereadonlyisProxy--> 检查对象是否是由 reactive 或 readonly 创建的 proxyisReactive--> 检查对象是否是由 reactive 创建的响应式代理isReadonly--> 检查对象是否是由 readonly 创建的只读代理toRowmarkRowshallowReactiveshallowReadonly
# reactive
setup 中把他充当 Vue2 中的 data 函数来使用的;只是 setup 函数 return 出来; 对对象中的值为 ref 的属性自动解包;通过属性访问到的直接是值,而并非 ref 包;
这里我们必须明确的是经过
Tips
在 setup 函数中, return 之前,无论你操作原始对象还是响应式副本,数据会更新,并将最新的数据作用于视图;此处不要产生误解,好像操作原始数据能更新页面视图;但是其实在页面视图渲染之前数据已经发生更新;
问:更新原始数据之后,响应式副本的数据也已经发生了更新,那什么时候可以将更新的数据反应到视图上呢?
答:下一次响应式副本数据更新的时候
看到这是是不是觉得有点像 Vue2 中未产生响应式的状态
了解什么是响应式副本,我们先了解一个叫 Reflect (opens new window) 的特性;
# readonly
赋值警告:
Set operation on key "${String(key)}" failed: target is readonly.
删除属性警告:
Delete operation on key "${String(key)}" failed: target is readonly.
# toRow
reactive, readonly 代理的原始对象;主要作用就是为了实现上面讲 reactive 提到的数据更新,视图不更新的场景;但是大部分时候呢它是作为一个优化的项来使用的;
- 在临时读取数据的时候,从经过
toRow处理过的原始对象读取数据无需承担代理访问/跟踪的开销; - 在视图更新给紧急的时候,先做数据更新,等待所有数据处理完毕之后,将所有更新的数据进行一次性的更新,这样避免多次触发视图更新;
# markRow
在了解了 toRow 之后,看
# shallowReactive
reactive 一样,都是产生响应式副本,但是 ref 的属性并不会自动解包,通过属性访问到的依然是 ref 包;
# shallowReadonly
ref 的属性也不会产生只读效果,无论是不是根属性;其实这里也很好理解,ref 包裹后的基础类型数据也是一个对象;ref 解包;
# ref
reactive 的作用一样,都是定义响应式数据的;既然功能类似为什么 Vue3 要提供两个 API 呢?
虽然功能类似,但是还说区别的:
ref返回的响应式数据是一个ref对象;reactive返回的是一个 Proxy 数据;ref代理的数据必须通过ref对象的内部属性value访问,value是一个 Proxy 数据;ref产生响应式数据的过程是先接受一个数据作为ref对象的内部属性value的值;然后对内部属性value做响应式处理;reactive是直接将接受到的对象做响应式处理,并返回一个响应式副本;ref可以用来代理引用数据;reactiveVue3 不能用来代理基础类型数据, 直接返回原始值;
# 说到这里:
问:为什么官方推荐基础类型的数据推荐使用 ref 而 引用类型数据推荐使用 reactive 呢?
答: Vue3 的 Proxy 只能为引用类型的数据提供代理服务, 无法为基础类型数据提供代理服务;
问:为什么 ref 能将基础类型数据通过 Proxy 代理?
答:ref 函数接收一个值,作为内部对象的 value 属性的值,返回的是一个 ref 对象;本质上就是将接收的值放到 ref 对象上,然后使用 Proxy 对 ref 对象进行代理;
问:同样是引用类型数据 ref 和 reactive 如何取舍?
答:如果你想对数据的每次更新都是重新赋值,那么建议使用 ref, 如果只是对对象的属性进行更新,那么建议使用 reactive
重点
- ref 不仅只能对基础类型数据做响应时代理
- reactive 一直只能对引用类型数据做响应式代理
- ref 代理的数据在 js 中访问必须通过
.value访问,在模板中使用会自动解包,直接使用即可; - 基础类型数据:
string,number,boolean,bigint,null,undefined,symbol
# unRef
如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数;
# toRef
可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
白话将就是可以将响应式对象中的某个属性结构出来,并且这个属性还将与响应式对象保持响应式链接,不会因为被解构而失去响应式;
const state = reactive({
a: 1
})
const a = toRef(state, 'a')
a++
console.log(state.a) // 2
# toRefs
批量解构响应式对象,功能同 toRef
# customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象;
<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello')
}
}
}
其实意义不大,类似情况应该在我们应该我们的业务层代码去处理,而并非实现一个自定的 ref
# shallowRef
创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。类似于 shallowReactive;
# triggerRef
手动执行与 shallowRef 关联的任何副作用。
# computed
computed 没有任何区别;
下面来看一下在 setup 函数中如何使用:
import { computed, reactive } from 'vue'
const state = reactive({
name: 'town'
})
const name = computed(() => state.name)
state.name = 'town2021'
console.log(name) // town2021
// 当然我们还可以创建一个可以修改的计算属性
const name = computed({
get: () => state.name,
set: val => state.name === val
})
name = 'town2020'
console.log(state.name) // town2020
总结一句话,getter 函数, 并从 getter 返回值中返回一个不变的响应式 ref 对象;
# watchEffect / watch
watchEffect 的作用跟 watch 类似;
但是也有几点不同:
watch惰性执行副作用,watchEffect会立即执行;- 更具体地说明应触发侦听器重新运行的状态
- 访问被侦听状态的先前值和当前值
第一点在上文已经提到了
第二点,大家来看一段代码;
const state = {
name: 'town',
age: 20
}
// 并未声明我要监听什么,但是该函数只会监听state.name的变化
const watchEffect(() => {
if (state.name) {
console.log(state.name)
}
})
// 显式的声明了当前watch监听的是state.name
watch(() => state.name, (val, oval) => {
console.log(val, oval)
})
// 其实这段代码也说明了第三点的不同,watch 的回调接收两个参数,一个是state.name的当前状态值,一个是更新之前的值
现在来看一下 watchEffect 和 watch 都是如何监听多个属性变化的;
const state = {
name: 'town',
age: 20
}
// watchEffect 监听多个属性变化,
// name 和 age 变化的时候都会触发 watchEffect 的执行
const watchEffect(() => {
if (state.name) {
console.log(state.name)
}
if (state.age === 22) {
console.log(state.age)
}
})
// watch 监听多个属性变化
// 回调函数接收两个数组,分别是监听元素的当前值数组和更新之前的值数组
watch([state.name, state.age], ([name, age], [oname, oage]) => {
console.log(name, age)
})
问:为什么 watchEffect 不需要显式的声明监听的数据也能正确的监听数据的变化;
# 新增组合式 API
# setup
propscontext
# props
props: 显式声明在
props选项中的属性, props 跟 Vue2 中的 props 一样,是响应式的,所以不建议使用 ES6 解构,会消除 props 的响应式;
如果一定要进行解构且希望保留响应式的话,Vue3 也为我们提供了安全的方法:
props: {
name: String,
age: Number
},
setup (props) {
// 第一种解构方式
const { name } = toRefs(props)
// 第二种解构方式
const newProps = { ...toRefs(props) }
// 第三种解构方式
const name = toRef(props, 'title')
}
// 这三种解构方式都可保持响应式, 只要是被使用toRefs之后的数据后续无论你再如何进行解构都将保持响应式连接;
// 第三种解构方式的主要是为了解决当传入的props中不存在name的情况下,toRefs将不会为name创建一个 ref 对象,
// 所以name也就不存在了,后续技术props传入了name,那么我们结构出来的数据也不会是响应式的;
推荐使用第一种解构方式,在 JS 中使用,按需解构;切记请勿将 props 中结构出来的数据使用在模板上
问:如何能让产生了响应式的数据失去响应式?
# context
context: 因为在
setup函数执行的时候,组件实例还未被创建,所以在context中仅有attrs,slots,emit这三个属性;context 只是一个普通的对象,所以允许解构;
attrs:父组件传入的属性,非响应式slots:当前组件的插槽,非响应式emit:触发父组件定义的自定义事件的触发器
# 渲染函数
h 函数返回的结果;也可以返回一段 JSX;
h 函数的渲染
setup () {
const state = reactive({
title: '组件'
})
return () => h('div', {}. 'h函数的渲染' + state.title)
}
jsx 渲染
setup () {
const state = reactive({
title: '组件'
})
return () => (
<div>jsx渲染{state.title}</div>
)
}
推荐使用 JSX 写法,dom 解构,数据逻辑更清晰;
注意
setup 函数中 this 并非当前活跃实例的引用;如果需要引用当前活跃实例,请使用 getCurrentInstance 函数;
# 组合 API 生命周期钩子
# 选项 API 生命周期选项和组合式 API 之间的映射
-> 使用beforeCreatesetup()~~ created~~-> 使用setup()beforeMount->onBeforeMountmounted->onMountedbeforeUpdate->onBeforeUpdateupdated->onUpdatedbeforeUnmount->onBeforeUnmountunmounted->onUnmountederrorCaptured->onErrorCapturedrenderTracked->onRenderTrackedrenderTriggered->onRenderTriggeredactivated->onActivateddeactivated->onDeactivated