你需要知道的Vue3到底哪里有变化

1,371 阅读9分钟

一、 新增特性 New

Async components异步组件

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

// Async component without options
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// Async component with options
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

新的defineAsyncComponent helper方法显式地定义了异步组件 component选项重命名为loader 加载器函数本身并不接受resolve和reject参数,并且必须返回一个Promise实例

emits Option

Vue 3 提供了一个 emits option,类似于 props option. 这个 option 可用于定义组件可以发送给其父组件的事件。

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

emits option还接受一个对象,它允许开发人员为随发出的事件传递的参数定义验证器,类似于props定义中的验证器

const app = Vue.createApp({})

// Array syntax
app.component('todo-item', {
  emits: ['check'],
  created() {
    this.$emit('check')
  }
})

// Object syntax
app.component('reply-form', {
  emits: {
    // no validation
    click: null,

    // with validation
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }
})

Fragments

在Vue 3中,组件现在正式支持多根节点组件,也就是fragments

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

二、删除的特性 remove

$children

$children instance属性已从Vue 3.0中移除,不再受支持。如果您需要访问子组件实例,我们建议使用$refs。

Filters 过滤器

fliters过滤器已从Vue 3.0中移除,不再支持。相反,建议用methods或computed计算属性替换它们。

$listeners

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

events API

vue3中完全删除了onon、off和$once方法

v-on.native

v-on的.native修饰符已经被删除,vue3中,新的emits option 允许child定义它确实发出哪些事件。

因此,Vue3将把所有没有定义为child中组件触发事件的事件监听器添加到 child的根元素中(除非 child的选项中设置了inheritAttrs: false)。

<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
<!-- MyComponent.vue -->
<script>
  export default {
    emits: ['close']
  }
</script>

三、修改的特性 breaking

v-for Array Refs

<div v-for="item in list" :ref="setItemRef"></div>
import { ref, onBeforeUpdate, onUpdated } from 'vue'

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

在Vue 3中,这样的使用不再会自动在$refs中创建数组。要从单个绑定中检索多个引用,请将ref绑定到提供更多灵活性的函数(这是一个新特性)

itemRefs不一定是一个数组,也可以是一个对象,其中引用由它们的迭代键设置。

如果需要的话,这也允许对项目引用进行reactive和watched。

$attrs 包含class和style

class和style在Vue 2的虚拟DOM实现中得到了一些特殊的处理。由于这个原因,它们没有包含在attrs中,而其他所有属性都包含在attrs中,而其他所有属性都包含在attrs中。

当使用inheritAttrs: false时,会出现这样的副作用:$attrs中的属性不再自动添加到根元素,而是由开发人员决定添加到哪里。但是class和style不是$attrs的一部分,仍然会被应用到组件的根元素上。

在Vue 3中,$attrs现在包含传递给组件的所有属性,包括class和style。

<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中渲染的Html如下-->
<label class="my-class">
  <input type="text" id="my-id" />
</label>

<!--vue3中渲染的Html如下-->
<label>
  <input type="text" id="my-id" class="my-class" />
</label>

注意:在使用inheritAttrs: false的组件中,确保样式仍然按照预期工作。如果以前依赖于类和样式的特殊行为,那么一些视觉效果可能会被破坏,因为这些属性现在可能被应用到另一个元素。

Custom Directives自定义指令

vue3中,指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。

created:在应用元素的属性或事件监听器之前调用此函数。(新增)

bind → beforeMount

inserted → mounted

beforeUpdate:这是在更新元素之前调用的,很像组件生命周期钩子。(新增)

update → 有太多的相似之处更新,所以这是多余的,已删除。请使用updated代替。

componentUpdated → updated

beforeUnmount:类似于组件生命周期钩子,它将在卸载元素之前被调用。(新增)

unbind → unmounted

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

vue3中的指令可以这样使用

<p v-highlight="'yellow'">Highlight this text bright yellow</p>
const app = Vue.createApp({})

app.directive('highlight', {
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value
  }
})

Custom Elements Interop自定义元素交互操作

如果我们想添加一个在Vue外部定义的定制元素(例如使用Web Components API),我们需要“指示”Vue将其视为定制元素。让我们以下面的模板为例。

<plastic-button></plastic-button>

Vue 2中,通过Vue.config.ignoredElements将标签白名单为自定义元素

Vue.config.ignoredElements = ['plastic-button']

在Vue 3中,这个检查是在模板编译期间执行的。指示编译器将视为自定义元素:

如果使用构建步骤:将isCustomElement选项传递给Vue模板编译器。如果使用vue-loader,这应该通过vue-loader的compilerOptions选项传递

// in webpack config
rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button'
      }
    }
  }
  // ...
]

如果使用动态模板编译,请通过app.config.isCustomElement传递:

const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'

需要注意的是,运行时配置只影响运行时模板编译——它不会影响预编译模板。

Customized Built-in Elements 自定义的内置的元素

Custom Elements规范提供了一种将自定义元素作为自定义内置元素(打开新窗口)使用的方法,方法是将is属性添加到内置元素

在Vue3中,我们将Vue对is道具的特殊处理仅限于标签。

  • 当在保留的标签上使用时,它的行为和2.x中的完全一样;

  • 当在普通组件上使用时,它的行为将像一个普通属性:

<button is="plastic-button">Click Me!</button>
  • 2.x行为:呈现bar组件。
  • 3.x行为:渲染foo组件并传递is属性。
<foo is="bar" />
  • 2.x行为:渲染plastic-button组件。
<button is="plastic-button">Click Me!</button>
  • 3.x行为:通过调用来呈现本地按钮
document.createElement('button', { is: 'plastic-button' })

在Vue 2中,我们建议通过在本地标签上使用is道具来解决这些限制:

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

Vue3中,随着is的行为改变,我们引入了一个新的指令v-is来处理这些情况:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>

v-is函数就像动态的vue2中的 :is binding,所以要通过注册名来渲染组件,它的值应该是一个JavaScript字符串。

data option

vue2中,data option只能被定义成object或function

vue3中,data option 已经标准化,只接受返回对象的函数

<script>
  import { createApp } from 'vue'

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

Mixin合并行为改变

当一个组件的data()及其mixins或extends基类被合并时,vue3中的合并是浅层执行的

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

const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}
// 在vue2中合并的结果是:
{
  user: {
    id: 2,
    name: 'Jack'
  }
}
// 在vue3中合并的结果是:
{
  user: {
    id: 2
  }
}

Functional Components 函数组件

在Vue 2中,功能组件有两个主要用例: 使用组件,它负责渲染出适当的标题(例如,h1, h2, h3等),这可以在2中写成一个单文件组件

// Vue 2 Functional Component Example
export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}

或者

<!--Vue 2 Functional Component Example with -->
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

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

现在在Vue 3中,所有功能组件都是用普通函数创建的。换句话说,不需要定义{functional: true}组件选项。

它们将收到两个参数:props和context。context参数是一个对象,它包含组件的attrs、slot和emit属性。

此外,h现在是全局导入的,而不是在渲染函数中隐式地提供h。

import { h } from 'vue'

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

DynamicHeading.props = ['level']

export default DynamicHeading

Single File Components (SFCs)

functional属性在<template>中被移除 监听器现在作为$attrs的一部分传递,可以删除

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

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

Global API 全局API

  • createApp 调用createApp会返回一个app实例,这是Vue 3中的一个新概念
    import { createApp } from 'vue'
    const app = createApp({})
    

    所有全局改变Vue行为的api现在都移动到应用实例中。

    2.x Global API3.x Instance API (app)
    Vue.configapp.config
    Vue.config.productionTipremoved
    Vue.config.ignoredElementsapp.config.isCustomElement
    Vue.componentapp.component
    Vue.directiveapp.directive
    Vue.mixinapp.mixin
    Vue.useapp.use
    Vue.prototypeapp.config.globalProperties
  • Provide / Inject

    Vue 3应用实例还可以提供依赖项,这些依赖项可以被应用内部的任何组件注入

    // in the entry
    app.provide('guide', 'Vue 3 Guide')
    
    // in a child component
    export default {
      inject: {
        book: {
          from: 'guide'
        }
      },
      template: `<div>{{ book }}</div>`
    }
    

    在编写插件时,使用provide尤其有用,它可以替代全局属性

    Global API Treeshaking

在Vue 3中,全局api和内部api都进行了重组,因此,全局api现在只能作为ES模块构建的命名导出访问。例如:

import { nextTick } from 'vue'

nextTick(() => {
  // something DOM-related
})

直接调用Vue.nextTick()现在会导致的undefined is not a function错误。

有了这个更改,如果模块绑定器tree-shaking,Vue应用程序中不使用的全局api将从最终绑定包中删除,从而得到最佳的文件大小。

以下vue2中的指令都收到了tree-shaking的影响:

  • Vue.nextTick
  • Vue.observable (被 Vue.reactive替换)
  • Vue.version
  • Vue.compile (仅在完整版本中)
  • Vue.set (仅在compat构建中)
  • Vue.delete (仅在compat构建中)

内部变化

除了公共api之外,许多内部组件/帮助程序现在也被导出为命名导出。这允许编译器输出仅在使用特性时才导入的代码。例如:

<transition>
  <div v-show="ok">hello</div>
</transition>

被编译成类似于下面的内容:

import { h, Transition, withDirectives, vShow } from 'vue'

export function render() {
  return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])
}

这实际上意味着只有当应用程序真正使用转换组件时,才会导入它。换句话说,如果应用程序没有任何组件,那么支持该特性的代码将不会出现在最终的bundle中。

通过全局tree-shaking,用户只为他们实际使用的功能“付费”。更好的是,如果知道可选特性不会增加不使用它们的应用程序的包大小,那么框架大小就不再是未来额外核心特性的考虑因素了。

如果你使用像webpack这样的模块包,这可能会导致Vue的源代码被绑定到插件中,这通常不是你所期望的。防止这种情况发生的一个常见做法是配置模块绑定程序,将Vue从最终的绑定包中排除。在webpack的情况下,你可以使用externals(打开新窗口)配置选项:

// webpack.config.js
module.exports = {
  /*...*/
  externals: {
    vue: 'Vue'
  }
}

这将告诉webpack把Vue模块作为一个外部库,而不是bundle它。

keyattribute

在vue3中,不再推荐在v-if/v-else/v-else-if分支上使用key属性,因为如果你不提供唯一的键,现在在条件分支上会自动生成它们。

<template v-for>

Vue 2中<template>标签不能有键值。相反,您可以将键放在它的每个子节点上:

<!-- Vue 2.x -->
<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>

Vue 3中,key应该放在<template>标签上。

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

当使用<template v-for>和一个子元素使用v-if时,键应该移动到<template>标签。

<!-- Vue 2.x -->
<template v-for="item in list">
  <div v-if="item.isVisible" :key="item.id">...</div>
  <span v-else :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div v-if="item.isVisible">...</div>
  <span v-else>...</span>
</template>

KeyCode Modifiers 键码修饰符

因为KeyboardEvent。keyCode已被弃用(打开新窗口),Vue 3继续支持它也不再有意义。因此,现在建议对任何希望用作修饰符的键使用kebab-case名称。

<!-- Vue 3 Key Modifier on v-on -->
<input v-on:keyup.delete="confirmDelete" />

Render Function API

在Vue2中,render函数将自动接收h函数(这是createElement的常规别名)作为参数:

// Vue 2 Render Function Example
export default {
  render(h) {
    return h('div')
  }
}

在Vue3中, h现在是全局导入的,而不是自动作为参数传递。

// Vue 3 Render Function Example
import { h } from 'vue'

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

注册组件

在Vue2中,如果一个组件已经注册,render函数将组件的名称以字符串形式传递给第一个参数:

// 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中,由于vnode与上下文无关,不能再使用字符串ID隐式地查找已注册的组件,这里我们需要使用一个导入的resolveComponent方法:

// 3.x
import { h, resolveComponent } from 'vue'

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

slots统一

当使用渲染函数时,例如 h,在Vue2中用于定义内容节点上的slot数据属性。

// 2.x Syntax
h(LayoutComponent, [
  h('div', { slot: 'header' }, this.header),
  h('div', { slot: 'content' }, this.content)
])

此外,当引用作用域槽时,可以使用以下语法引用它们

// 2.x Syntax
this.$scopedSlots.header

在Vue3中, slot被定义为当前节点作为对象的子节点

// 3.x Syntax
h(LayoutComponent, {}, {
  header: () => h('div', this.header),
  content: () => h('div', this.content)
})

当需要以编程方式引用限定范围的插槽时,它们现在统一到$slots选项中。

// 2.x Syntax
this.$scopedSlots.header

// 3.x Syntax
this.$slots.header()

Transition Class 变化

v-enter转换类被重命名为v-enter-from, v-leave转换类被重命名为v-leave-from。

/* vue2中*/
.v-enter,
.v-leave-to {
  opacity: 0;
}

.v-leave,
.v-enter-to {
  opacity: 1;
}
/* vue3中*/
.v-enter-from,
.v-leave-to {
  opacity: 0;
}

.v-leave-from,
.v-enter-to {
  opacity: 1;
}

Transition Group Root Element

默认情况下不再渲染根元素,但仍然可以用tag 创建。

在Vue 2中,,像其他定制组件一样,需要一个根元素,默认情况下是,但可以通过tag定制。

<transition-group tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</transition-group>

在Vue 3中,我们提供了fragment支持,组件不再需要根节点。因此,在默认情况下不再渲染。

如果您已经在Vue 2代码中定义了标记道具,就像上面的例子一样,一切都将像以前一样工作; 如果你没有定义,而你的样式或其他行为依赖于根元素的存在来正常工作,只需添加tag="span"到

<transition-group tag="span">
  <!-- -->
</transition-group>

v-model

  • 当在自定义组件上使用时,v-model的prop和event的默认名称会改变
    • props: value -> modelValue;
    • event: input->update: modelValue;
  • v-bind的.sync修饰符和组件model option被移除,并被v-model的一个参数替换;
  • 新功能:现在可以在同一个组件上创建多个v-model绑定;
  • 新功能:增加了创建自定义v-model的能力。

在Vue2中,在组件上使用v-model等价于传递一个value并发出一个input事件:

<ChildComponent v-model="pageTitle" />

<!-- would be shorthand for: -->

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

如果我们想改变prop或event的名称,我们需要在ChildComponent组件中添加一个model option:

<!-- ParentComponent.vue -->

<ChildComponent v-model="pageTitle" />
// ChildComponent.vue

export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    // this allows using the `value` prop for a different purpose
    value: String,
    // use `title` as the prop which take the place of `value`
    title: {
      type: String,
      default: 'Default title'
    }
  }
}

所以,v-model在这里可以简写成:

<ChildComponent :title="pageTitle" @change="pageTitle = $event" />

在某些情况下,我们可能需要对一个prop进行“双向绑定”(有时需要为不同的prop添加现有的v-model)。为此,我们建议以update:myPropName模式发出事件。例如,在上一个带有title prop的例子中,我们可以通过以下方式来传达给ChildComponent赋值的意图:

this.$emit('update:title', newValue)

然后parent可以监听该事件并更新本地数据属性。例如:

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

可以简写成 .sync的修改方式:

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

在vue3中,在自定义组件上的 v-model等价于传递modelValue prop并发出一个update:modelValue事件:

<ChildComponent v-model="pageTitle" />

<!-- would be shorthand for: -->

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

v-model arguments

要更改一个 model的名称, 而不是model component选项 , 现在我们传递一个参数给v-model:

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

<!-- would be shorthand for: -->

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

这也可以作为.sync修饰符的替代,并允许我们在自定义组件上拥有多个v-model。

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

<!-- would be shorthand for: -->

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

v-model修改器

除了vue2硬编码的v-model修饰符,如 .trim,现在是vue3支持自定义修饰符:

<ChildComponent v-model.capitalize="pageTitle" />

v-if 和 v-for的优先级

在vue3中,v-if总是比v-for优先级高

v-bind合并行为

在Vue2中,如果一个元素同时定义了v-bind="object"和一个相同的单个属性,那么单个属性将总是覆盖对象中的绑定。

<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="red"></div>

在Vue3中,如果一个元素同时定义了v-bind="object"和一个相同的单独属性,那么绑定声明的顺序决定了它们如何被合并。换句话说,与假设开发人员希望单个属性总是覆盖对象中定义的内容不同,开发人员现在对所需的合并行为有更多的控制。

<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>

<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>

watch监听数组

在Vue3中,当使用watch选项来监视一个数组时,回调只会在数组被替换时触发。换句话说,watch回调将不再当数组突变时被触发。若要在突变时触发,必须指定deep选项。

watch: {
  bookList: {
    handler(val, oldVal) {
      console.log('book list changed')
    },
    deep: true
  },
}