Vue3-03 从Vue2迁移
Vue3学习笔记-03 从Vue2迁移
Teleport
一个新增的内置组件,用来提供一种干净的方法,允许我们控制在DOM哪个父节点下渲染HTML,不必求助于全局状态或者将其拆分为两个组件
1 | <teleport to="body"> |
Vue会将<teleport>标签内的内容渲染为<body>标签的子级
它接受两个参数,第一个参数to是有效的查询选择器,指定要挂载的目标元素,第二个参数disabled用来禁用<teleport>的功能,元素会在原组件位置渲染
注意,disabled状态变化后,将移动实际的DOM节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的活动状态(例如播放的视频等)
在同一个目标上使用多个<teleport>组件,会将内容挂载到同一个目标元素,顺序就是简单的追加,稍后挂载的将位于目标元素中较早的挂载之后
v-for中的Ref数组
从单个绑定获取多个Ref时,需要将ref绑定到一个更灵活的函数上,在函数中,将函数的参数el推书预置的数组中:
1 | <div v-for="item in list" :ref="setItemRef"></div> |
选项式API:
1 | export default { |
组合式API:
1 | import { onBeforeUpdate, onUpdated } from 'vue' |
注意:
itemRefs不必是数组,可以是一个对象- 如果需要,
itemRef也可以是响应式的,可以被监听 - 需要在
onBeforeUpdate中对itemRefs重置,否则会出问题
异步组件(新增)
Vue3中函数式组件被定义为纯函数,异步组件的定义需要通过defineAsyncComponent方法显示定义:
1 | import { defineAsyncComponent } from 'vue' |
相比于Vue2,component选项被重命名为loader,loader函数不接受resolve和reject参数,必须始终返回Promise
1 | const { createApp, defineAsyncComponent } = Vue |
如果利用Webpack和ES2015的能力,可以使用import直接导入:
1 | import { defineAsyncComponent } from 'vue' |
defineAsyncComponent的完整用法:
1 | import { defineAsyncComponent } from 'vue' |
$attrs包含class和style
Vue2中v-bind="$attrs"会把除了class和style的属性应用到元素上,而Vue3中的$attrs则会包含class和style
组件的
inheritAttrs默认为true,这样加载组件的不被认作Props的Attribute会应用到子组件的根元素上,通过设置inheritAttrs为false,这个默认行为会被取消,通过配合v-bind="$attrs"会把这些Attribute显性的绑定到非根元素上
$children
Vue2中可以通过$children访问当前组件实例的子组件,这个API在Vue3中取消了,需要使用ref来访问子组件
自定义指令
Vue3中自定义指令的钩子有了很大变化:
created(new):在元素的Attribute或事件侦听器创建前调用bind→beforeMountinserted→mountedbeforeUpdated(new):在元素本身更新前调用update移除,改为使用updatedcomponentUpdated→updatedbeforeUnmount(new),在卸载元素前调用unbind→unmounted
最终API如下:
1 | const MyDirective = { |
Vue中通过binding.vnode访问组件实例:
1 | mounted(el, binding, vnode) { |
Data选项
Vue3的Data选项只接受返回object的function,不再接受object本身
Data的Mixin合并
在Vue2中,来自组件的Data与来自Mixin或者Extends的Data合并时,会执行深层次的合并,在Vue3中只会执行浅层次的合并:
1 | const Mixin = { |
上面的Mixin,在Vue2中得到的data是:
1 | { |
在Vue3中,得到的data是:
1 | { |
对于依赖Mixin深度合并的行为,建议重构代码以完全避免这种依赖,因为Mixin的深度合并非常隐式,这让代码逻辑难以理解和调试
emits选项
Vue3中新增了emits选项,与props选项很类似。这个选项用来定义和校验(包括类型定义)组件能想父组件触发什么事件
1 | const app = Vue.createApp({}) |
emits与props一样,可以接受对象作为验证参数
移除了v-on.native修饰符
Vue中,传递给带有v-on的组件的事件监听器,只有通过this.$emit触发,要将原生DOM监听器添加到子组件的根元素上,需要使用.native修饰符
1 | <my-component |
在Vue3中,.native已经被移除,同时,上面提到的emits选项允许组件定义真正会被触发的事件,而为包含在emits选项中的事件监听器,Vue会把它们作为原生事件监听器添加到子组件的根元素中(除非在子组件中设置了inheritAtts: false)
1 | <script> |
这样click事件就会添加到my-component的根元素上
要特别注意,需要将要出发的事件添加到emits中,否则就会出现事件被触发两次的问题,例如:
1 | <template> |
父组件中为mu-button添加click监听器:
1 | <my-button v-on:click="handleClick"></my-button> |
这时click会被触发两次:
emit触发一次- 因为
emits选项中未定义,所以my-button上定义的handleClick会被添加到子组件的根元素上,再次被触发
事件API
Vue2中的$on、$off、$once方法被移除了,也就是说在Vue3中,无法在通过下面的形式创建全EventHub全局事件监听器:
1 | // eventHub.js |
Vue3移除了上述API,推荐使用mitt或者tiny-emitter来实现全局事件监听器
过滤器
Vue2中的过滤器被移除了,建议使用方法调用或者计算属性替换
如果在应用中定义了全局过滤器,可以通过全局属性在所有组件中使用:
1 | // main.js |
然后通过$filters对象修改所有模板:
1 | <template> |
注意此方式只能应用在方法中,不能在计算属性中使用,因为计算属性只有在单个组件的上下文中定义才有意义
多根节点组件支持
Vue中不支持多根节点组件,所以需要使用<div>包裹多个组件:
1 | <template> |
Vue3中组件支持包含多个根节点,但是需要开发者显示定义Attribute应该分布在哪个元素或者组件上:“
1 | <!-- Layout.vue --> |
全局API
createApp
Vue中通过new Vue()创建根Vue实例,从同一个Vue构造函数创建的每个根实例共享相同的全局配置,导致了两个问题:
- 测试期间,全局配置很容易污染测试用例
- 同一个页面上的多个Vue副本无法使用不同的全局配置
1 | // 这会影响两个根实例 |
为了避免这个问题,引入了新的全局APIcreateApp,调用它返回一个应用实例:
1 | import { createApp } from 'vue' |
在Vue中任何全局改变Vue的行为的API都移动到了应用实例上:
config.ingoredElements替换为config.isCustomElement
使用config.isCustomElement可以支持原生自定义元素,取值是一个函数,符合这个函数返回值的标签名都不会编译为Vue组件,而是作为原生的自定义元素出现
1 | // Vue2.x |
Vue3中元素是否是Vue组件的检查是在模板编译阶段完成的,因此只有在使用运行时编译器才考虑配置此选项,回事在Vue CLI的配置中新的顶层选项
Vue.prototype替换为config.globalProperties
Vue2中通过为Vue的原型Vue.prototype添加属性,让所有组件都可以访问, 例如:
1 | // 之前 - Vue 2 |
在Vue中被替换为config.globalProperties:
1 | // 之后 - Vue 3 |
如果在选项是API中,仍然使用this.$http访问
要注意,如果使用了TypeScript,直接访问this.$http会报错,需要在shims-vue.d.ts添加如下的定义:
1 | // Vue 原型上添加的东西,需要在此定义 |
具体可以参考这里。
使用provide在编写插件时非常有用,可以替代globalProperties。
全局Tree Shaking
在Vue2中,例如Vue.nextTick等AAPI,无法通过Webpack的tree-shaking移除,在Vue3.0中对全局和内部API进行了冲过,全局API只能通过ES模块构建的命名导出进行文旦,例如使用nextTick时:
1 | import { nextTick } from 'vue' |
这样可以更好的支持Webpack的tree-shaking,Vue应用程序中未使用的全局API会从最终打包的产出中消除,减小打包体积
key属性
key不能相同,并且在Vue2中,<template>不能拥有key属性,需要为每个子节点设置key:
1 | <!-- Vue 2.x --> |
在Vue3中则可以设置在<template>上了
1 | <!-- Vue 3.x --> |
按键修饰符
Vue2中支持keyCodes作为修改v-on方法的途径:
1 | <!-- 键码版本 --> |
由于keyboradEvent.keyCode不再被推荐使用,所以建议对任何要用作修饰符的键使用kebab-cased大小写名称
移除$listeners
Vue2中的事件监听器通过$listeners访问,在Vue3中$listeners被移除了,事件监听器是$attrs的一部分,事件监听器现在只是以on为前缀的Attribute
Props默认函数中不能访问this
生成Prop默认值的工厂函数不能再访问this(虽然我以前也没这么用过),可以将组件接收到的原始Prop作为参数传递给默认函数,也可以在默认函数中使用Inject API
1 | import { inject } from 'vue' |
渲染函数
- 函数
h从Vue中导入,不再作为参数传递给render函数 - 渲染函数参数更改,有状态组件和函数组件之间更一致
- VNode的Prop结构更扁平
- 注册组件时不能在使用字符串ID隐式查找已注册组件,需要使用导入的
resolveComponent方法
1 | // 2.x |
详细的参考渲染函数的介绍。
插槽统一
移除了$scopedSlots,同时插槽定义为当前节点的子对象:
1 | // 3.x Syntax |
需要以编程方式引用时,都被统一到了$slots中:
1 | // 2.x 语法 |
过渡CSS类名更改
v-enter更改为v-enter-from,v-leave更改为v-leave-from
<transition-group>过渡组件
<transition-group>不再默认渲染根元素,但仍然可以用tagprop创建根元素。
v-model和.sync
Vue2中
Vue2中在组件上使用v-model相当于绑定了value的Prop和input事件:
1 | <ChildComponent v-model="pageTitle" /> |
如果需要将属性或事件名更改为其他名称,需要在子组件中添加model选项进行修改:
1 | <!-- ParentComponent.vue --> |
1 | // ChildComponent.vue |
某些情况下需要对一个Prop进行『双向绑定』,在Vue中可以使用update:propName来抛出事件,例如对于上面的ChildComponent来说,可以通过下面的方法更新父元素中的title:
1 | this.$emit('update:title', newValue) |
父组件中可以监听该事件并更新本地数据:
1 | <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" /> |
而.sync修饰符就是上面的代码的语法糖:
1 | <ChildComponent :title.sync="pageTitle" /> |
Vue3中
Vue3里面,自定义组件的v-model相当于传递了modelValue的Prop,并接受抛出的update:modelValue事件:
1 | <ChildComponent v-model="pageTitle" /> |
如果要更改v-model传递的Prop名称,不再需要去子组件中配置model选项,而是给v-model传递一个参数即可:
1 | <ChildComponent v-model:title="pageTitle" /> |
这也可以作为.sync的替代,所以.sync就没有用武之地了,所以在Vue2中的sync直接替换为v-model即可
1 | <ChildComponent :title.sync="pageTitle" /> |
同时在同一个组件上,可以同时使用多个v-model:
1 | <ChildComponent v-model:title="pageTitle" v-model:content="pageContent" /> |
v-model自定义修饰符
Vue2中v-model提供了.trim、lazy、.number内置修饰符,Vue3中还提供了自定义修饰符的能力。
添加到v-model的修饰符通过modelModifiersProp 提供给组件,它接受一个default的对象,取值是一个函数,这个函数会返回一个对象,当父组件没有提供v-model的修饰符时,会从这个函数返回的对象里面取值
注意,
modelModifiers是一个Prop,不是Vue实例的选项,它提供的并不是修饰符具体逻辑的实现,而是提供组件默认包含哪些修饰符,取值是布尔值
请注意,当组件的created生命周期钩子触发时,modelModifiersProp会包含capitalize,且其值为true——因为capitalize被设置在了写为v-model.capitalize="myText"的v-model绑定上。
建议把组件中定义的修饰符显式的写在modelModifiersProp中,这样可以为TypeScript版本的组件提供正确的类型推导,也便于一眼看出组件中使用了哪些修饰符
另外,v-model自定义修饰符也支持多级串联
1 | <v-model-child v-model.capitalize.test="msg" /> |
在v-model-child组件中,我们会检查props.modelModifiers是否包含capitalize和test,并且编写一个处理器来更改emit发出的值:
1 | <template> |
对于带有参数的(即自定义model使用的Prop名的v-model)v-model绑定,生成的自定义修饰符的Prop名称为参数名 + "Modifiers":
1 | <my-component v-model:description.capitalize="myText"></my-component> |
1 | app.component('my-component', { |
v-for与v-if的优先级
Vue2中,在同一个元素上应用v-if和v-for,v-for优先级更高,而Vue3中v-if的优先级更高,也就是说v-if将没有权限访问v-for里面的变量,下面的代码会报错:
1 | <!-- This will throw an error because property "todo" is not defined on instance. --> |
所以要尽量避免在同一个元素上使用v-for和v-if,可以把v-for移动到<template>中来修正:
1 | <template v-for="todo in todos" :key="todo.name"> |
v-bind合并行为
Vue2中,如果一个元素同时定义了v-bind="object"和一个相同的单独的Property,那么单独的Property会覆盖object中的绑定:
1 | <!-- template --> |
在Vue3中覆盖结果取决于声明绑定的书序,后面绑定的结果会覆盖前面的绑定结果:
1 | <!-- template --> |
对数组的监听
在Vue3中,使用watch监听一个数组时,默认只有数组被替换时才会触发回调,数组成员改变时不会触发回调,如果希望在数组改变时触发需要添加deep参数
1 | watch: { |