Vue2.x 转 Vue3.0 兼容问题【持续更新】

1,481 阅读11分钟

Vue3.0 除了增加了组合API,对Vue2.x 的一些API以及配置项都有一些差异,这里除了参考官网列举的Vue2.x迁移Vue3.x的步骤以外,会收集日常开发遇到的差异问题,持续更新~

传送门-官网
传送门-迁移

要兼容vue2.x的语法,需要安装 @vue/compat , 并设置 @vue/compat 的别名为 vue ,可进行全局兼容配置:

  • vue-cli
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.resolve.alias.set('vue', '@vue/compat')

    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return {
          ...options,
          compilerOptions: {
            // 兼容配置
            compatConfig: {
              MODE: 2
            }
          }
        }
      })
  }
}
  • webpack
// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      vue: '@vue/compat'
    }
  },
  module: {
    rules: [
      {
        test: /.vue$/,
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            // 兼容配置
            compatConfig: {
              MODE: 2
            }
          }
        }
      }
    ]
  }
}
  • vite
// vite.config.js
export default {
  resolve: {
    alias: {
      vue: '@vue/compat'
    }
  },
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // 兼容配置
          compatConfig: {
            MODE: 2
          }
        }
      }
    })
  ]
}

除了全局兼容配置,也支持单独兼容配置:

  • 组合式API configureCompat
import { configureCompat } from 'vue'

// 禁用某些兼容性特性
configureCompat({
  FEATURE_ID_A: false,
  FEATURE_ID_B: false
})
  • configureCompat 选项
export default {
  compatConfig: {
    MODE: 3, // 只为这个组件选择性启用 Vue 3 行为
    FEATURE_ID_A: true // 也可以在组件级别开启某些特性
  }
  // ...
}

compatConfig 内容为编译的兼容配置,主要分为:

  • ✔ 完全兼容
  • ◐ 部分兼容且附带注意事项
  • ⨂ 不兼容 (只有警告)
  • ⭘ 仅兼容 (没有警告)

具体查看

MODE:[2,3]: 指定编译的默认版本

非兼容 -〉✔ 完全兼容

v-model/.sync【非兼容

迁移构建开关:

  • COMPONENT_V_MODEL

  • COMPILER_V_BIND_SYNC

  • vue2.x

默认只能使用一个 v-model,自动注入 value prop 和 input event

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

可以通过 model: { prop:string, event:string } 修改 v-model 行为

<template>
    <ChildComponent v-model="pageTitle" />
</template>
<script>
// ChildComponent.vue
export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    // 这将允许 `value` 属性用于其他用途
    value: String,
    // 使用 `title` 代替 `value` 作为 model 的 prop
    title: {
      type: String,
      default: 'Default title'
    }
  }
}
</script>

<!-- 是以下的简写: -->
<ChildComponent :title="pageTitle" @change="pageTitle = $event" />

如果需要双向绑定多个字段,则属性[prop]需要设置 .sync 修饰符,同时提供 update:[prop] 事件触发数据更新

<ChildComponent :title.sync="pageTitle" />

<!-- 是以下的简写: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
  • vue3.x

差异:vue3.x 支持使用v-model同时绑定多个属性[prop],因此.sync就不是必须的,所以vue3弱化了此功能,需要兼容需要使用 @vue/compat 进行兼容配置

vue3.x中 v-model 未指定参数名,则会自动注入 modelValue 属性 和 update:modelValue 事件

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->

<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

可以通过指定参数[prop]绑定多个参数,自动注入 [prop] 属性 和 update:[prop] 事件

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 是以下的简写: -->

<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>

除此之外,vue3.x 的 v-model 还支持自定义修饰符 [modify],如果 v-model 未指定参数,自动在vm实例注入 modelModifiers 字段,Object 类型,记录各个修饰符的状态modify:Boolean,可通过读取字段判断是否配置自定义修饰符;如果 v-model 指定参数 arg, 则自动在 vm 实例注入 [arg]Modifoers 字段获取修饰符配置。

v-model 未指定参数,自动注入 modelModifiers

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
})

v-model 指定参数 description,自动注入 descriptionModifiers

<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text"
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})

filter 过滤器【移除

迁移构建开关:

  • FILTERS
  • COMPILER_FILTERS

从 Vue3.x 开始,filter过滤器已移除,且不再支持。推荐使用 computed计算属性 或者 methods方法 进行替换。

v-if/v-for【非兼容

v-if / v-for 使用上没有太大差别,主要差异在同时在一个元素上使用两种指令,进行模板编译时:

vue2.x 中 v-for 的优先级比 v-if

<template v-for="item in list" v-if="list.length">...</template>

// 模板编译,v-for优先级高,所以先执行v-for的循环,每个循环都会带上v-if进行判断,
// 但其实我们想要的不过是只有v-if为true才执行v-for

// 如果v-if="truth(item)"依赖每次循环的数据还是符合使用场景的。

vue3.x 中 v-if 的优先级比 v-for

<template v-for="item in list" v-if="list.length">...</template>

// 模板编译,v-if优先级高,所以先执行v-if的条件,为true时才执行v-for,看着是符合我们的预想

//如果我们的v-if="truth(item)"是依赖于每次循环的数据呢?

所以,还是建议 两条指令单独写,多写个template的事情

v-for 的ref引用【非兼容

迁移构建开关:

  • V_FOR_REF

  • COMPILER_V_FOR_REF

  • vue2.x 中在v-for使用ref,默认会在$refs生成一个ref数组字段,需要通过指定所以获取对应的子组件。然而 $refs 只会在组件渲染完成之后生效,并且它们不是响应式的。

  • vue3.x 使用新特性,不再自动创建 $ref 数组,要从单个绑定(v-for)获取多个 ref,需要将 ref 绑定到一个更灵活的函数上function(ref:vm):void {}

<div v-for="item in list" :ref="setItemRef"></div>

结合选项式 API:

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

可以自行设置每个循环的子组件ref到指定的变量,不一定是数组收集,所以:

  • itemRefs 不必是数组:它也可以是一个对象,其 ref 可以通过迭代的 key 被设置。
  • 如有需要,itemRefs 也可以是响应式的,且可以被侦听。(reactive、ref)

$listeners 字段移除【非兼容

迁移构建开关:

  • INSTANCE_LISTENERS

$listeners 对象在 Vue 3 中已被移除。事件监听器现在是 $attrs 的一部分:

  • vue2.x 会将元素上的v-on 事件收集到 $listeners 字段上,配合inheritAttrs: true:true`, 可以为非根元素的元素进行事件传递绑定。
<template>
  <label>
    <input type="text" v-bind="$attrs" v-on="$listeners" />
  </label>
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>

inheritAttrs:true 默认会把非prop属性挂在根元素,事件指定 .native 修饰符会把事件挂到根元素上。

  • 事件监听器现在只是以 on 为前缀的 attribute,这样它就成为了 $attrs 对象的一部分,所以去掉了 $listeners
<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
export default {
  inheritAttrs: false
}
</script>

// {
//   id: 'my-input',
//   onClose: () => console.log('close 事件被触发')
// }

$children 字段移除【非兼容

迁移构建开关:

  • INSTANCE_CHILDREN

$children 实例 property 已从 vue3.x 中移除,不再支持。

  • 在 vue2.x 中,开发者可以使用 this.$children 访问当前实例的直接子组件
  • 在 vue3.x 中,$children property 已被移除,且不再支持。如果你需要访问子组件实例,建议使用 $refs

mount/$mount【非兼容

在 vue2.x 中,使用 $mount 或者 根组件指定 el 选项挂载应用时,渲染应用的内容会替换要挂载的目标元素。在 vue3.x 中,使用 mount 挂载应用,但是渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML

同一个index.html模板

<body>
  <div id="app">
    Some app content
  </div>
</body>
  • vue2.x 挂载应用
new Vue({
  el: '#app',
  data() {
    return {
      message: 'Hello Vue!'
    }
  },
  template: `
    <div id="rendered">{{ message }}</div>
  `
})

// 或
const app = new Vue({
  data() {
    return {
      message: 'Hello Vue!'
    }
  },
  template: `
    <div id="rendered">{{ message }}</div>
  `
})

app.$mount('#app')

渲染后,直接替换目标节点

<body>
  <div id="rendered">Hello Vue!</div>
</body>
  • vue3.x 挂载应用
const app = Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  },
  template: `
    <div id="rendered">{{ message }}</div>
  `
})

app.mount('#app')

渲染后,作为子节点插入目标节点

<body>
  <div id="app" data-v-app="">
    <div id="rendered">Hello Vue!</div>
  </div>
</body>

defineCustomElement 自定义元素 (Web Components)【新增

vue模板解析时,如果标签不是HTML标签,优先当成声明的组件进行解析渲染,如果解析失败才会作为自定义元素渲染展示。

如果你希望 Vue 能将某些确切的元素作为自定义元素处理直接跳过组件解析,请指定 compilerOptions.isCustomElement 选项

  • 浏览器内配置示例
// 仅当使用浏览器内编译时有效
// 如果你正在使用构建工具,请查看下方的配置示例
app.config.compilerOptions.isCustomElement = tag => tag.includes('-')
  • Vite 配置示例
// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // 将所有包含短横线的标签作为自定义元素处理
          isCustomElement: tag => tag.includes('-')
        }
      }
    })
  ]
}
  • Vue CLI 配置示例
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // 将所有以 ion- 开头的标签作为自定义元素处理
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}

vue3.x提供组合式API defineCustomElement 进行自定义元素声明:

<my-vue-element></my-vue-element>
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
  // 这里是普通的 Vue 组件选项
  props: {},
  emits: {},
  template: `...`,
  // 只用于 defineCustomElement:注入到 shadow root 中的 CSS
  styles: [`/* inlined css */`]
})
// 注册该自定义元素。
// 注册过后,页面上所有的 `<my-vue-element>` 标记会被升级。
customElements.define('my-vue-element', MyVueElement)
// 你也可以用编程的方式初始化这个元素:
// (在注册之后才可以这样做)
document.body.appendChild(
  new MyVueElement({
    // 初始化的 prop (可选)
  })
)

customElements 是 Web Components 的API,通过customElements.define(custom-name:string, customElement:HTMLElement)注册自定义元素

SFC 作为自定义元素,只需使用 .ce.vue 作为文件拓展名即可:

import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'

console.log(Example.styles) // ["/* 内联的 css */"]

// 转换为自定义元素构造器
const ExampleElement = defineCustomElement(Example)

// 注册
customElements.define('my-example', ExampleElement)

自定义元素使用插槽,只能使用slot(Web Components 语法),不能使用v-slot,不支持作用域插槽:

<my-element>
  <div slot="named">hello</div>
</my-element>

通过 this.$emit 或在 setup 中的 emit 发出的事件会被调度为原生事件,事件额外参数会作为 target.details

自定义元素规范提供了一种将自定义元素作为自定义内置元素的方法,方法是向内置元素添加 is attribute:

<button is="plastic-button">点击我!</button>

is在vue中是特殊的prop

  • 在 vue2.x 中,它将被解释为渲染一个名为 plastic-button 的 Vue 组件,这将阻碍上面所提到的自定义内置元素的原生用法。

  • 在 vue3.x 中,我们将 Vue 对 is attribute 的特殊处理限制在了 <component> 标签中,进行组件解析;如果在普通组件上使用时,它的行为将类似于普通 attribute;如果在普通组件上使用想渲染成组件,则需要指定一个 vue: 前缀。

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

解析DOM模板【非兼容

有些 HTML 元素,诸如 <ul><ol><table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr> 和 <option>,只能出现在其它某些特定的元素内部。

但我们通过 import *.html,并作为template

当我们使用这些有约束条件的元素时会导致一些问题。例如:

<table>
    <blog-post-row></blog-post-row>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。

我们可以通过特殊的 is attribute 实现变通:

  • vue2.x 使用 is 指定替换组件
<table>
    <tr is="blog-post-row"></tr>
</table>
  • vue3.x 使用 is 指定替换组件, 需要使用 vue:前缀,否则is被当作attribute处理
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在

异步组件加载【新增

迁移构建开关:

  • COMPONENT_ASYNC

  • vue2.x 异步加载,通过异步组件工厂函数 Promise

// 全局注册
Vue.component(
    'async-webpack-example', // 这个动态导入会返回一个 `Promise` 对象。
    () => import('./my-async-component')
)

// 局部注册
new Vue({
    // ...
    components: {
        'my-component': () => import('./my-async-component')
    }
})

// vue2.4 支持异步加载状态配置
const AsyncComponent = () => ({
    // 需要加载的组件 (应该是一个 `Promise` 对象)
    component: import('./MyComponent.vue'),
    // 异步组件加载时使用的组件,可配置骨架屏或者加载提示组件进行占位展示
    loading: LoadingComponent,
    // 加载失败时使用的组件
    error: ErrorComponent,
    // 展示加载时组件的延时时间。默认值是 200 (毫秒)
    delay: 200,
    // 如果提供了超时时间且组件加载也超时了, // 则使用加载失败时使用的组件
    默认值是:`Infinity`
    timeout: 3000
})
  • vue3.x 提供组合式API defineSyncComponent 异步引用组件,可以接受一个返回 Promise 的工厂函数(纯函数),高级用法component 字段变成 loader 字段
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)
// 全局注册
app.component('async-component', AsyncComp)

// 局部注册
createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.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()
    }
  }
})

vue3.x 提供另一种异步加载方式 Suspense,是一个试验性的新特性,其 API 可能随时会发生变动。建议暂时不适用

<suspense> 组件提供了另一个方案,允许将等待过程提升到组件树中处理,而不是在单个组件中。

<template>
  <suspense>
    <template #default>
      <todo-list />
    </template>
    <template #fallback>
      <div>
        Loading...
      </div>
    </template>
  </suspense>
</template>

<script>
export default {
  components: {
    TodoList: defineAsyncComponent(() => import('./TodoList.vue'))
  }
}
</script>

<suspense> 组件有两个插槽,它们都只接收一个直接子节点

  • default 插槽:子节点会尽可能展示出来
  • fallback 插槽:default插槽未完全展示时,则展示fallback插槽(占位组件)

重要的是,异步组件不需要作为 <suspense> 的直接子节点。它可以出现在组件树任意深度的位置,且不需要出现在和 <suspense> 自身相同的模板中。只有所有的后代组件都准备就绪,该内容才会被认为解析完毕。

后代组件可以在 setup 函数中通过 async 实现返回 Promise,触发 suspense 的 fallback

export default {
  // 返回Promise,在loadData完成前展示fallback
  async setup() {
    // 在 `setup` 内部使用 `await` 需要非常小心
    // 因为大多数组合式 API 函数只会在
    // 第一个 `await` 之前工作
    const data = await loadData()

    // 它隐性地包裹在一个 Promise 内
    // 因为函数是 `async` 的
    return {
      // ...
    }
  }
}

v-bind 合并行为【非兼容

迁移构建开关:

  • COMPILER_V_BIND_OBJECT_ORDER

v-bind 的绑定顺序会影响渲染结果。在一个元素上动态绑定 attribute 时,同时使用 v-bind="object" 语法和独立 attribute 是常见的场景。然而,这就引出了关于合并的优先级的问题。

  • vue2.x 如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object 中的绑定。
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>
  • vue3.x 如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="blue"></div>

<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 结果 -->
<div id="red"></div>

attribute 强制行为【非兼容

这是一个底层的内部 API 更改,绝大多数开发人员不会受到影响。

以下是对变化的总体概述:

  • 移除枚举 attribute 的内部概念,并将这些 attribute 视为普通的非布尔 attribute
  • 非兼容:如果值为布尔值 false,则不再移除 attribute。取而代之的是,它将被设置为 attr="false"。若要移除 attribute,应该使用 null 或者 undefined

attribute 行为:

  • vue2.x

    • 对于“布尔 attribute”和 xlinks,如果它们是 falsy (undefinednull 或 false) 的,Vue 会移除它们,否则会加上。
    • 对于“枚举 attribute” (目前来说包括 contenteditabledraggable 和 spellcheck),Vue 会尝试将它们强制转换为字符串 (目前对 contenteditable 做了特殊处理,以修复 vuejs/vue#9397)。
    • 对于其他 attribute,我们将移除 falsy 的值 (undefinednull,或 false),其他值按原样设置。
绑定表达式foo 正常draggable 枚举boolean 布尔型
:attr="null"-draggable="false"-
:attr="undefined"---
:attr="true"foo="true"draggable="true"boolean="true"
:attr="false"-draggable="false"-
:attr="0"foo="0"draggable="true"boolean="true"
attr=""foo=""draggable="true"boolean="true"
attr="foo"foo="foo"draggable="true"boolean="true"
attrfoo=""draggable="true"boolean="true"
  • vue3.x
    • 【改动】对于非布尔型 attribute,如果其值为 false,Vue 将不再移除它们,而是将其强制转换为 'false'
绑定表达式foo 正常draggable 枚举boolean 布尔型
:attr="null"-- *-
:attr="undefined"---
:attr="true"foo="true"draggable="true"boolean="true"
:attr="false"foo="false" *draggable="false"-
:attr="0"foo="0"draggable="0" *boolean="true"
attr=""foo=""draggable="" *boolean="true"
attr="foo"foo="foo"draggable="foo" *boolean="true"
attrfoo=""draggable="" *boolean="true"

*表示有变动
布尔型 attribute 的强制转换保持不变。

$attr 包含 class和style【非兼容

迁移构建开关:

  • INSTANCE_ATTRS_CLASS_STYLE

$attrs 现在包含了所有 传递给组件的 attribute,包括 class 和 style,当然还有 $listeners

<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
export default {
  inheritAttrs: false
}
</script>

使用组件

<my-component id="my-id" class="my-class"></my-component>
  • vue2.x 在实现虚拟DOM时,classstyle 特殊处理没有加入 $attrs 中,$attrs 中的字段会自动添加到根元素中。如果使用 inheritAttrs: false 时会产生副作用:
    • $attrs 中的 attribute 将不再被自动添加到根元素中,而是由开发者决定在哪添加。
    • 但是 class 和 style 不属于 $attrs,它们仍然会被应用到组件的根元素中:=

渲染后

<label class="my-class">
  <input type="text" id="my-id" />
</label>
  • vue3.x $attrs 包含了所有的 attribute

渲染后

<label>
  <input type="text" id="my-id" class="my-class" />
</label>

Data 选项【非兼容

迁移构建开关:

  • OPTIONS_DATA_FN
  • OPTIONS_DATA_MERGE

vue3.x的data选项变化:

  • 组件选项 data 的声明不再接收纯 JavaScript object,而是接收一个 function
  • 当合并来自 mixin 或 extend 的多个 data 返回值时,合并操作现在是浅层次的而非深层次的 (只合并根级属性)。

在 vue2.x 中,开发者可以通过 object 或者是 function 定义 data 选项。

<!-- Object 声明 -->
<script>
  const app = new Vue({
    data: {
      apiKey: 'a1b2c3'
    }
  })
</script>

<!-- Function 声明 -->
<script>
  const app = new Vue({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  })
</script>

在 vue3.x 中,data 选项已标准化为只接受返回 object 的 function

<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  }).mount('#app')
</script>

当来自组件的 data() 及其 mixin 或 extends 基类被合并时

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}

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

在 vue2.x 中,生成的 $data 是:(进行深合并)

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

在 vue3.x 中,生成的 $data 是:(进行浅合并,只对根级字段进行合并)

{
  "user": {
    "id": 2
  }
}

emits 选项【新增

vue3.x 现在提供一个 emits 选项,和现有的 props 选项类似,可以用来定义一个组件可以向其父组件触发的事件类型。未在emits选项声明的事件,会记录在$attrs中,否则,会从$attrs移除。

<script>
  export default {
    props: ['text'],
    emits: ['accepted']
    ...
  }
</script>

更多信息请参阅emits API

事件API ($on、$off、$once)【非兼容

迁移构建开关:

  • INSTANCE_EVENT_EMITTER

$on$off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口。

  • vue2.x 中,利用vm实例的事件API($on$off 和 $once)触发事件,可用于创建一个事件总线,作为整个应用中可用的全局事件监听器:
// eventBus.js

const eventBus = new Vue()

export default eventBus

组件使用事件总线监听事件

// ChildComponent.vue
import eventBus from './eventBus'

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

父子组件/兄弟组件都可以通过事件总线触发事件

// ParentComponent.vue / BrotherComponents.vue
import eventBus from './eventBus'

export default {
  methods: {
    callGlobalCustomEvent() {
      eventBus.$emit('custom-event') // 当 ChildComponent 已被挂载时,控制台中将显示一条消息
    }
  }
}
  • vue3.x 中vm实例完全移除了 $on$off 和 $once 方法。$emit 仍然包含于现有的 API 中,仍需要用于进行直接父子组件通信。

事件总线 可以通过第三方库实现,例如 mitt 或 tiny-emitter

或者通过createApp传递根组件事件、或者使用vuex实现事件通信。

根组件事件

createApp(App, {
  // 监听 'expand' 事件
  onExpand() {
    console.log('expand')
  }
})

函数式组件【非兼容

迁移构建开关:

  • COMPONENT_FUNCTIONAL

函数式组件只能由接收 props 和 context (即:slotsattrsemit) 的普通函数创建

  • 非兼容functional attribute 已从单文件组件 (SFC) 的 <template> 中移除

  • 非兼容{ functional: true } 选项已从通过函数创建的组件中移除

  • vue2.x 定义函数组件,可以作为性能优化,因为它们的初始化速度比有状态组件快得多;同时函数组件可以返回多个根节点

// Vue 2 函数式组件示例
export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}

或者,对于喜欢在单文件组件中使用 <template> 的用户:

<!-- Vue 2 结合 <template> 的函数式组件示例 -->
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
  • vue3.x 所有的函数式组件都是用普通函数创建,将接收两个参数:props 和 context,可以使用 h API生成Vnode:
import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading

单文件组件SFC,不需要使用 functional attribute,不需要使用$listeners继承事件(已移至$attrs

<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>

内联模板 (inline-template)【非兼容

迁移构建开关:

  • COMPILER_INLINE_TEMPLATE

vue3.x 移除对内联模板特性的支持。

  • vue2.x 中,给子组件提供了 inline-template attribute,以便将其节点内容作为模板使用,而不是作为插槽内容分发:
<my-component inline-template>
  <!--div 内容将作为组件my-conponent的模版template-->
  <div>
    <p>它们将被编译为组件自己的模板,</p>
    <p>而不是父级所包含的内容。</p>
  </div>
</my-component>
  • vue3.x 已经移除此功能。推荐使用 插槽 或者 <script type="x-template"> 定义模版

在 vue3.x 中,由于原生片段的支持,插槽也可以渲染为根节点!

按键修饰符

迁移构建开关:

  • CONFIG_KEY_CODES
  • V_ON_KEYCODE_MODIFIER

vue3.x 中

  • 非兼容:不再支持使用数字 (即键码) 作为 v-on 修饰符

  • 非兼容:不再支持 config.keyCodes

  • vue2.x 使用 config.keyCodes 对键码定义别名,并都可以 作为 v-on 修饰符

定义 config.keyCodes

Vue.config.keyCodes = {
  f1: 112
}

使用键码、别名作为修饰符

<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />

<!-- 自定义别名版本 -->
<input v-on:keyup.f1="showHelpText" />
  • vue3.x 废弃 config.keyCodes,建议使用 kebab-cased (短横线) 名称作为修饰符的键
<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">

<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit">

语法的限制导致某些特定字符无法被匹配,比如 "'/=> 和 .。对于这些字符,你应该在监听器内使用 event.key 代替

渲染函数API 更改 createElement API / h【非兼容

vue3.x 提供组合式API h 供用户创建Vnode:

-   `h` 现在是全局导入,而不是作为参数传递给渲染函数
-   更改渲染函数参数,使其在有状态组件和函数组件的表现更加一致
-   VNode 现在有一个扁平的 prop 结构
  • vue2.x 定义render函数会自动带上h函数,作为(createElement函数别名),组件实例也挂载一个$createElement字段供组件创建Vnode
// Vue 2 渲染函数示例
export default {
  render(h) {
    return h('div')
  }
}
  • vue3.x h 函数通过全局导入的,而不是作为参数自动传递,因此render函数不在支持h参数,可直接在setup函数使用返回vnode:
// Vue 3 渲染函数示例
import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}

export default {
  setup(props, context) {
    return h('div')
  }
}
  • vue2.x Vnode prop 包含嵌套字段(如事件 on)
// 2.x
{
  staticClass: 'button',
  class: { 'is-outlined': isOutlined },
  staticStyle: { color: '#34495E' },
  style: { backgroundColor: buttonColor },
  attrs: { id: 'submit' },
  domProps: { innerHTML: '' },
  on: { click: submitForm },
  key: 'submit-button'
}
  • vue3.x Vnode prop 更加扁平化(如事件 on 开头)
// 3.x 语法
{
  class: ['button', { 'is-outlined': isOutlined }],
  style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
  id: 'submit',
  innerHTML: '',
  onClick: submitForm,
  key: 'submit-button'
}
  • vue2.x 中,注册一个组件后,可以把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作:
// 2.x
Vue.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      Clicked {{ count }} times.
    </button>
  `
})

export default {
  render(h) {
    return h('button-counter')
  }
}
  • vue3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件,需要使用一个resolveComponent 组合式API解析组册的组件:
// 3.x
import { h, resolveComponent } from 'vue'

export default {
  setup() {
    const ButtonCounter = resolveComponent('button-counter')
    return () => h(ButtonCounter)
  }
}

Transition 作为根节点【非兼容

当使用 <transition> 作为根结点的组件从组件外部被切换时将不再触发过渡效果。

  • vue2.x通过使用 <transition> 作为一个组件的根节点,过渡效果存在从组件外部触发的可能性:
<!-- 模态组件 -->
<template>
  <transition>
    <div class="modal"><slot/></div>
  </transition>
</template>

使用组件,可能触发transition过度效果,这并不是transition的实现目的,<transition> 原本是希望被其子元素触发的,而不是 <transition> 自身。

<!-- 用法 -->
<modal v-if="showModal">hello</modal>
  • vue3.x 组件外部不会触发组件根节点为transition的过渡效果,如果需要组件外部控制展示show,可通过prop传入字段控制
<template>
  <transition>
    <div v-if="show" class="modal"><slot/></div>
  </transition>
</template>
<script>
export default {
  props: ['show']
}
</script>
<!-- 用法 -->
<modal :show="showModal">hello</modal>

Transition Group 根元素【非兼容

迁移构建开关:

  • TRANSITION_GROUP_ROOT

<transition-group> 不再默认渲染根元素span,但仍然可以用 tag attribute 创建根元素。

  • vue2.x <transition-group> 像其它自定义组件一样,需要一个根元素。默认的根元素是一个 <span>,但可以通过 tag attribute 定制。
<transition-group tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</transition-group>
  • vue3.x 有片段的支持,因此组件不再需要根节点,所以,<transition-group> 不再默认渲染根节点。如果仍然定义 tag attribute,那么效果会和之前一样。
<transition-group tag="span">
  <div v-for="item in items" :key="item">
    {{ item }}
  </div>
</transition-group>

key attribute【非兼容

  • 新增:对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key

    • 非兼容:如果你手动提供 key,那么每个分支必须使用唯一的 key。你将不再能通过故意使用相同的 key 来强制重用分支。
  • 非兼容<template v-for> 的 key 应该设置在 <template> 标签上 (而不是设置在它的子节点上)

v-if/v-else/v-else-if上定义key

  • vue2.x 建议定义唯一的key(不唯一其实也不会影响运行)
<!-- Vue 2.x -->
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
  • vue3.x 推荐自己定义key,vue自己会生成唯一的key,如果自己手动指定key,需要保证唯一。
<!-- Vue 3.x -->
<!-- 自动生成key,推荐 -->
<div v-if="condition">Yes</div>
<div v-else>No</div>

<!-- 手动指定 -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="b">No</div>

使用<template v-for>进行列表渲染

  • vue2.x 不允许在template 定义key,并且如果template有多个子节点,需要对所有子节点都指定key,或者用一个父节点包裹(多余节点)。
<!-- Vue 2.x -->
<template v-for="item in list">
  <div :key="'heading-' + item.id">...</div>
  <span :key="'content-' + item.id">...</span>
</template>

<template v-for="item in list">
  <div :key="'parent-' + item.id">
      <div>...</div>
      <span>...</span>
  </div>
</template>
  • vue3.x 则需要在template指定key,减少多余节点创建
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

自定义指令【非兼容

迁移构建开关:

  • CUSTOM_DIR

指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。同时,expression 字符串不再作为 binding 对象的一部分被传入,能够直接在 binding 访问组件实例

  • 在 vue2.x 中,自定义指令通过使用下列钩子来创建,以对齐元素的生命周期,它们都是可选的:

    • bind - 指令绑定到元素后调用。只调用一次。
    • inserted - 元素插入父 DOM 后调用。
    • update - 当元素更新,但子元素尚未更新时,将调用此钩子。
    • componentUpdated - 一旦组件和子级被更新,就会调用这个钩子。
    • unbind - 一旦指令被移除,就会调用这个钩子。也只调用一次。
  • 在 vue3.x 中,自定义指令的钩子函数进行了重命名,统一了组件的生命周期钩子:

    • created - 【新增】在元素的 attribute 或事件监听器被应用之前调用。
    • bind → beforeMount
    • inserted → mounted
    • beforeUpdate:【新增】在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
    • update → updated
    • componentUpdated → updated
    • beforeUnmount:【新增】与组件的生命周期钩子类似,它将在元素被卸载之前调用。
    • unbind -> unmounted

如果需要在自定义指令访问组件实例

  • vue2.x 需要通过 vnode 获取 vnode.context

    bind(el, binding, vnode) {
      const vm = vnode.context
    }
    
  • vue3.x 能够直接在binding访问

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

.native 修饰符

迁移构建开关:

  • COMPILER_V_ON_NATIVE

v-on 的 .native 修饰符已被移除

  • vue2.x 默认情况下,组件指定 v-on 的事件监听器只能通过组件调用 this.$emit 触发。如果要将原生 DOM 的事件监听器添加到子组件的根元素中,需要使用 .native 修饰符:
// 把原生 click 事件挂载到组件my-component 的根节点上
<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>
  • vue3.x 移除.native新增的 emits 选项指定了组件需要通过this.$emit触发的事件,否则事件都将被收集到$attrs字段(on开头,如onClick),默认挂载到组件根节点上,可通过inheritAttrs:false修改$attrs挂载节点。具体查看

插槽统一 $slots/$scopedSlots非兼容

迁移构建开关:

  • INSTANCE_SCOPED_SLOTS

vue3.x 统一了组件中的普通插槽$slots和作用域插槽$scopedSlots

  • this.$slots 返回所有插槽内容,每个插槽以函数表示
  • 非兼容:移除 this.$scopedSlots 字段

使用 createElement创建节点

  • vue2.x 需要指定 slot
// 2.x 语法 需要指定 slot
h(LayoutComponent, {}, [
  h('div', { slot: 'header' }, this.header),
  h('div', { slot: 'content' }, this.content)
])
  • vue3.x 插槽以对象的形式定义为当前节点的子节点
// 3.x Syntax
h(LayoutComponent, {}, {
  header: () => h('div', this.header),
  content: () => h('div', this.content)
})

获取作用域插槽:

// 2.x 语法
this.$scopedSlots.header

// 3.x 语法 作用域插槽,函数传入slotScoped
this.$slots.header()

在 prop 的默认函数中访问this非兼容

迁移构建开关: PROPS_DEFAULT_THIS

vue3.x prop定义默认值的工厂函数不再能访问 this

取而代之的是:

  • 组件接收到的原始 prop 将作为参数传递给默认函数;
  • inject API 可以在默认函数中使用。
import { inject } from 'vue'

export default {
  props: {
    theme: {
      default (props) {
        // `props` 是传递给组件的【新增】
        // 在任何类型/默认强制转换之前的原始值,
        // 也可以使用 `inject` 来访问注入的 property【新增】
        return inject('theme', 'default-theme')
      }
    }
  }
}

propsData【非兼容

propsData 选项用于在创建 Vue 实例的过程中传入 prop,vue3.0中已经移除。如果想为 vue3.x 应用的根组件传入 prop,请使用 createApp 的第二个参数。

  • vue2.x,在new 一个Vue组件实例传入:
const Comp = Vue.extend({
  props: ['username'],
  template: '<div>{{ username }}</div>'
})

new Comp({
  propsData: {
    username: 'Evan'
  }
})
  • vue3.x 向根组件传入prop:
const app = createApp(
  {
    props: ['username'],
    template: '<div>{{ username }}</div>'
  },
  { username: 'Evan' }
)

片段【新增

vue3.x 支持多个根节点的组件,也就是片段

  • 在 vue2.x 中,由于不支持多根节点组件,当其被开发者意外地创建时会发出警告。要修复这类问题,需要将多组件包裹在了一个 <div> 中。
<!-- Layout.vue -->
<template>
  <div><!--多余节点-->
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>
  • 在 vue3.x 中,组件可以包含多个根节点!但是,这要求开发者显式定义 attribute 应该挂载在哪个组件(只有一个根组件,attribute默认挂载根组件)。
<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>