Vue3-03 从Vue2迁移

文章目录

  • Teleport
  • `v-for`中的Ref数组
  • 异步组件(新增)
  • `$attrs`包含`class`和`style`
  • `$children`
  • 自定义指令
  • Data选项
  • Data的Mixin合并
  • `emits`选项
  • 移除了`v-on.native`修饰符
  • 事件API
  • 过滤器
  • 多根节点组件支持
  • 全局API
    • `createApp`
    • `config.ingoredElements`替换为`config.isCustomElement`
    • `Vue.prototype`替换为`config.globalProperties`
  • 全局Tree Shaking
  • `key`属性
  • 按键修饰符
  • 移除`$listeners`
  • Props默认函数中不能访问`this`
  • 渲染函数
  • 插槽统一
  • 过渡CSS类名更改
  • ``过渡组件
  • `v-model`和`.sync`
    • Vue2中
    • Vue3中
  • `v-model`自定义修饰符
  • `v-for`与`v-if`的优先级
  • `v-bind`合并行为
  • 对数组的监听

Teleport

一个新增的内置组件,用来提供一种干净的方法,允许我们控制在DOM哪个父节点下渲染HTML,不必求助于全局状态或者将其拆分为两个组件


  

Vue会将标签内的内容渲染为标签的子级

它接受两个参数,第一个参数to是有效的查询选择器,指定要挂载的目标元素,第二个参数disabled用来禁用的功能,元素会在原组件位置渲染

注意,disabled状态变化后,将移动实际的DOM节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的活动状态(例如播放的视频等)

在同一个目标上使用多个组件,会将内容挂载到同一个目标元素,顺序就是简单的追加,稍后挂载的将位于目标元素中较早的挂载之后

v-for中的Ref数组

从单个绑定获取多个Ref时,需要将ref绑定到一个更灵活的函数上,在函数中,将函数的参数el推书预置的数组中:

选项式API:

export default {
  data() {
    return {
      itemRefs: []
    }
  },
  methods: {
    setItemRef(el) {
      if (el) {
        this.itemRefs.push(el)
      }
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  },
  updated() {
    console.log(this.itemRefs)
  }
}

组合式API:

import { onBeforeUpdate, onUpdated } from 'vue'

export default {
  setup() {
    let itemRefs = []
    const setItemRef = el => {
      if (el) {
        itemRefs.push(el)
      }
    }
    onBeforeUpdate(() => {
      itemRefs = []
    })
    onUpdated(() => {
      console.log(itemRefs)
    })
    return {
      setItemRef
    }
  }
}

注意:

  • itemRefs不必是数组,可以是一个对象
  • 如果需要,itemRef也可以是响应式的,可以被监听
  • 需要在onBeforeUpdate中对itemRefs重置,否则会出问题

异步组件(新增)

Vue3中函数式组件被定义为纯函数,异步组件的定义需要通过defineAsyncComponent方法显示定义:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// 带选项的异步组件
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

相比于Vue2,component选项被重命名为loaderloader函数不接受resolvereject参数,必须始终返回Promise

const { createApp, defineAsyncComponent } = Vue

const app = createApp({})

const AsyncComp = defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      resolve({
        template: '
I am async!
' }) }) ) app.component('async-example', AsyncComp)

如果利用Webpack和ES2015的能力,可以使用import直接导入:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

app.component('async-component', AsyncComp)

defineAsyncComponent的完整用法:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  // 工厂函数
  loader: () => import('./Foo.vue')
  // 加载异步组件时要使用的组件
  loadingComponent: LoadingComponent,
  // 加载失败时要使用的组件
  errorComponent: ErrorComponent,
  // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
  delay: 200,
  // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
  // 默认值:Infinity(即永不超时,单位 ms)
  timeout: 3000,
  // 定义组件是否可挂起 | 默认值:true
  suspensible: false,
  /**
   *
   * @param {*} error 错误信息对象
   * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
   * @param {*} fail  一个函数,指示加载程序结束退出
   * @param {*} attempts 允许的最大重试次数
   */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      // 请求发生错误时重试,最多可尝试 3 次
      retry()
    } else {
      // 注意,retry/fail 就像 promise 的 resolve/reject 一样:
      // 必须调用其中一个才能继续错误处理。
      fail()
    }
  }
})

$attrs包含classstyle

Vue2中v-bind="$attrs"会把除了classstyle的属性应用到元素上,而Vue3中的$attrs则会包含classstyle

组件的inheritAttrs默认为true,这样加载组件的不被认作Props的Attribute会应用到子组件的根元素上,通过设置inheritAttrsfalse,这个默认行为会被取消,通过配合v-bind="$attrs"会把这些Attribute显性的绑定到非根元素上

$children

Vue2中可以通过$children访问当前组件实例的子组件,这个API在Vue3中取消了,需要使用ref来访问子组件

自定义指令

Vue3中自定义指令的钩子有了很大变化:

  • created(new):在元素的Attribute或事件侦听器创建前调用
  • bindbeforeMount
  • insertedmounted
  • beforeUpdated(new):在元素本身更新前调用
  • update移除,改为使用updated
  • componentUpdatedupdated
  • beforeUnmount(new),在卸载元素前调用
  • unbindunmounted

最终API如下:

const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {}, // 新
  updated() {},
  beforeUnmount() {}, // 新
  unmounted() {}
}

Vue中通过binding.vnode访问组件实例:

mounted(el, binding, vnode) {
  const vm = binding.instance
}

Data选项

Vue3的Data选项只接受返回objectfunction,不再接受object本身

Data的Mixin合并

在Vue2中,来自组件的Data与来自Mixin或者Extends的Data合并时,会执行深层次的合并,在Vue3中只会执行浅层次的合并:

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}
const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}

上面的Mixin,在Vue2中得到的data是:

{
  user: {
    id: 2,
    name: 'Jack'
  }
}

在Vue3中,得到的data是:

{
  user: {
    id: 2
  }
}

对于依赖Mixin深度合并的行为,建议重构代码以完全避免这种依赖,因为Mixin的深度合并非常隐式,这让代码逻辑难以理解和调试

emits选项

Vue3中新增了emits选项,与props选项很类似。这个选项用来定义和校验(包括类型定义)组件能想父组件触发什么事件

const app = Vue.createApp({})

// 数组语法
app.component('todo-item', {
  emits: ['check'],
  created() {
    this.$emit('check')
  }
})

// 对象语法
app.component('reply-form', {
  emits: {
    // 没有验证函数
    click: null,

    // 带有验证函数
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }
})

emitsprops一样,可以接受对象作为验证参数

移除了v-on.native修饰符

Vue中,传递给带有v-on的组件的事件监听器,只有通过this.$emit触发,要将原生DOM监听器添加到子组件的根元素上,需要使用.native修饰符


在Vue3中,.native已经被移除,同时,上面提到的emits选项允许组件定义真正会被触发的事件,而为包含在emits选项中的事件监听器,Vue会把它们作为原生事件监听器添加到子组件的根元素中(除非在子组件中设置了inheritAtts: false


这样click事件就会添加到my-component的根元素上

要特别注意,需要将要出发的事件添加到emits中,否则就会出现事件被触发两次的问题,例如:



父组件中为mu-button添加click监听器:


这时click会被触发两次:

  • emit触发一次
  • 因为emits选项中未定义,所以my-button上定义的handleClick会被添加到子组件的根元素上,再次被触发

事件API

Vue2中的$on$off$once方法被移除了,也就是说在Vue3中,无法在通过下面的形式创建全EventHub全局事件监听器:

// eventHub.js

const eventHub = new Vue()
export default eventHub

// ChildComponent.vue
import eventHub from './eventHub'

export default {
  mounted() {
    // 添加 eventHub 监听器
    eventHub.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // 移除 eventHub 监听器
    eventHub.$off('custom-event')
  }
}

// ParentComponent.vue
import eventHub from './eventHub'

export default {
  methods: {
    callGlobalCustomEvent() {
      eventHub.$emit('custom-event') // 当 ChildComponent 被挂载,控制台中将显示一条消息
    }
  }
}

Vue3移除了上述API,推荐使用mitt或者tiny-emitter来实现全局事件监听器

过滤器

Vue2中的过滤器被移除了,建议使用方法调用或者计算属性替换

如果在应用中定义了全局过滤器,可以通过全局属性在所有组件中使用:

// main.js
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}

然后通过$filters对象修改所有模板:


注意此方式只能应用在方法中,不能在计算属性中使用,因为计算属性只有在单个组件的上下文中定义才有意义

多根节点组件支持

Vue中不支持多根节点组件,所以需要使用

包裹多个组件:


Vue3中组件支持包含多个根节点,但是需要开发者显示定义Attribute应该分布在哪个元素或者组件上:“



全局API

createApp

Vue中通过new Vue()创建根Vue实例,从同一个Vue构造函数创建的每个根实例共享相同的全局配置,导致了两个问题:

  1. 测试期间,全局配置很容易污染测试用例
  2. 同一个页面上的多个Vue副本无法使用不同的全局配置
// 这会影响两个根实例
Vue.mixin({
  /* ... */
})

const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

为了避免这个问题,引入了新的全局APIcreateApp,调用它返回一个应用实例:

import { createApp } from 'vue'

const app = createApp({})

在Vue中任何全局改变Vue的行为的API都移动到了应用实例上:

Vue3-03 从Vue2迁移_第1张图片

config.ingoredElements替换为config.isCustomElement

使用config.isCustomElement可以支持原生自定义元素,取值是一个函数,符合这个函数返回值的标签名都不会编译为Vue组件,而是作为原生的自定义元素出现

// Vue2.x
Vue.config.ignoredElements = ['my-el', /^ion-/]

// Vue3.x
const app = createApp({})
app.config.isCustomElement = tag => tag.startsWith('ion-')

Vue3中元素是否是Vue组件的检查是在模板编译阶段完成的,因此只有在使用运行时编译器才考虑配置此选项,回事在Vue CLI的配置中新的顶层选项

Vue.prototype替换为config.globalProperties

Vue2中通过为Vue的原型Vue.prototype添加属性,让所有组件都可以访问, 例如:

// 之前 - Vue 2
Vue.prototype.$http = () => {};

// 组件中
this.$http();

在Vue中被替换为config.globalProperties

// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}

如果在选项是API中,仍然使用this.$http访问

要注意,如果使用了TypeScript,直接访问this.$http会报错,需要在shims-vue.d.ts添加如下的定义:

// Vue 原型上添加的东西,需要在此定义
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $http: () => {};
  }
}

具体可以参考这里。

使用provide在编写插件时非常有用,可以替代globalProperties

全局Tree Shaking

在Vue2中,例如Vue.nextTick等AAPI,无法通过Webpack的tree-shaking移除,在Vue3.0中对全局和内部API进行了冲过,全局API只能通过ES模块构建的命名导出进行文旦,例如使用nextTick时:

import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

这样可以更好的支持Webpack的tree-shaking,Vue应用程序中未使用的全局API会从最终打包的产出中消除,减小打包体积

key属性

key不能相同,并且在Vue2中,