是时候从vue2 迁移到 vue3了 (系列二)

2,010 阅读8分钟

下载.jpeg

上一篇文章:是时候从vue2 迁移到 vue3了 (系列一)

16.内联模板 Attribute 非兼容

变化概览:

  • 对内联特性的支持已被移除。

2.x 语法

在 2.x 中,Vue 为子组件提供了 inline-template attribute,以便将其内部内容用作模板,而不是将其作为分发内容。

<my-component inline-template>
  <div>
    <p>它们被编译为组件自己的模板</p>
    <p>不是父级所包含的内容。</p>
  </div>
</my-component>

3.x 语法

将不再支持此功能。

迁移策略

inline-template 的大多数用例都假设没有构建工具设置,所有模板都直接写在 HTML 页面中。

选项1:使用 <script> 标签

在这种情况下,最简单的解决方法是将

<script type="text/html" id="my-comp-template">
  <div>{{ hello }}</div>
</script>

在组件中,使用选择器指向目标模板:

const MyComp = {
  template: '#my-comp-template'
  // ...
}

这不需要任何构建设置,可以在所有浏览器中工作,不受任何内部 DOM HTML 解析警告的约束 (例如,你可以使用 camelCase prop 名称),并且在大多数 ide 中提供了正确的语法高亮显示。在传统的服务器端框架中,可以将这些模板拆分为服务器模板部分 (包括在主 HTML 模板中),以获得更好的可维护性。

选项2:默认插槽

以前使用 inline-template 的组件也可以使用默认插槽——进行重构,这使得数据范围更加明确,同时保留了内联编写子内容的便利性:

<!-- 2.x 语法 -->
<my-comp inline-template :msg="parentMsg">
  {{ msg }} {{ childState }}
</my-comp>

<!-- 默认 Slot 版本 -->
<my-comp v-slot="{ childState }">
  {{ parentMsg }} {{ childState }}
</my-comp>

子级现在应该渲染默认 slot*,而不是提供的空模板:

<!--
  在子模板中,在传递时渲染默认插槽
  在必要的 private 状态下。
-->
<template>
  <slot :childState="childState" />
</template>

17.key attribute 非兼容

变化概览:

  • 新增:对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。
    • 非兼容:如果你手动提供 key,那么每个分支必须使用唯一的 key。你不能通过故意使用相同的 key 来强制重用分支。
  • 非兼容:<template v-for> 的 key 应该设置在 <template> 标签上 (而不是设置在它的子节点上)。

背景

特殊的 key attribute 被用于提示 Vue 的虚拟 DOM 算法来保持对节点身份的持续跟踪。这样 Vue 可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。

在条件分支中

Vue 2.x 建议在 v-if/v-else/v-else-if 的分支中使用 key。

<!-- Vue 2.x -->
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>

这个示例在 Vue 3.x 中仍能正常工作。但是我们不再建议在 v-if/v-else/v-else-if 的分支中继续使用 key attribute,因为没有为条件分支提供 key 时,也会自动生成唯一的 key。

<!-- Vue 3.x -->
<div v-if="condition">Yes</div>
<div v-else>No</div>

非兼容变更体现在如果你手动提供了 key,那么每个分支都必须使用一个唯一的 key。因此大多数情况下都不需要设置这些 key。

<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>

<!-- Vue 3.x (recommended solution: remove keys) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>

<!-- Vue 3.x (alternate solution: make sure the keys are always unique) -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="b">No</div>

结合 <template v-for>

在 Vue 2.x 中 <template> 标签不能拥有 key。不过你可以为其每个子节点分别设置 key。

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

在 Vue 3.x 中 key 则应该被设置在 <template> 标签上。

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

类似地,当使用 <template v-for> 时存在使用 v-if 的子节点,key 应改为设置在 <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>

18.按键修饰符 非兼容

变化概览:

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

2.x 语法

在 Vue 2 中,支持 keyCodes 作为修改 v-on 方法的方法。

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

<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />

此外,你也可以通过全局 config.keyCodes 选项。

Vue.config.keyCodes = {
  f1: 112
}
<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />

<!-- 自定别名版本 -->
<input v-on:keyup.f1="showHelpText" />

3.x 语法

从KeyboardEvent.keyCode has been deprecated 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称。

<!-- Vue 3 在 v-on 上使用 按键修饰符 -->
<input v-on:keyup.delete="confirmDelete" />

因此,这意味着 config.keyCodes 现在也已弃用,不再受支持。

19.移除 $listeners 非兼容

变化概览:

  • $listeners 对象在 Vue 3 中已被移除。现在事件监听器是 $attrs 的一部分:
{
  text: 'this is an attribute',
  onClose: () => console.log('close Event triggered')
}

2.x 语法

在 Vue 2 中,你可以使用 this.$attrs 和 this.$listeners 分别访问传递给组件的 attribute 和事件监听器。结合 inheritAttrs: false,开发者可以将这些 attribute 和监听器应用到其它元素,而不是根元素:

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

3.x 语法

在 Vue 3 的虚拟 DOM 中,事件监听器现在只是以 on 为前缀的 attribute,这样就成了 $attrs 对象的一部分,因此 $listeners 被移除了。

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

如果这个组件接收一个 id attribute 和一个 v-on:close 监听器,那么 $attrs 对象现在将如下所示:

{
  id: 'my-input',
  onClose: () => console.log('close Event triggered')
}

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

生成 prop 默认值的工厂函数不再能访问 this。

替代方案:

  • 把组件接收到的原始 prop 作为参数传递给默认函数;

  • inject API 可以在默认函数中使用。

import { inject } from 'vue'

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

21. 渲染函数 API 非兼容

变化概览:

  • 此更改不会影响 <template> 用户。

  • 以下是更改的简要总结:

    • h 现在全局导入,而不是作为参数传递给渲染函数
    • 渲染函数参数更改以在有状态组件和函数组件之间更加一致
    • VNode 现在有一个扁平的 prop 结构

渲染函数参数

2.x 语法

在 2.x 中,render 函数会自动接收 h 函数 (它是 createElement 的传统别名) 作为参数:

// Vue 2 渲染函数示例
export default {
  render(h) {
    return h('div')
  }
}

3.x 语法

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

// Vue 3 渲染函数示例
import { h } from 'vue'

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

渲染函数签名更改

2.x 语法

在 2.x 中,render 函数自动接收诸如 h 之类的参数。

// Vue 2 渲染函数示例
export default {
  render(h) {
    return h('div')
  }
}

3.x 语法

在 3.x 中,由于 render 函数不再接收任何参数,它将主要用于 setup() 函数内部。这还有一个好处:可以访问在作用域中声明的响应式状态和函数,以及传递给 setup() 的参数。

import { h, reactive } from 'vue'

export default {
  setup(props, { slots, attrs, emit }) {
    const state = reactive({
      count: 0
    })

    function increment() {
      state.count++
    }

    // 返回渲染函数
    return () =>
      h(
        'div',
        {
          onClick: increment
        },
        state.count
      )
  }
}

VNode Prop 格式化

2.x 语法

在 2.x 中,domProps 包含 VNode prop 中的嵌套列表:

// 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'
}

3.x 语法

在 3.x 中,整个 VNode prop 的结构都是扁平的。使用上面的例子,来看看它现在的样子。

// 3.x 语法
{
  class: ['button', { 'is-outlined': isOutlined }],
  style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
  id: 'submit',
  innerHTML: '',
  onClick: submitForm,
  key: 'submit-button'
}

注册组件

2.x 语法

在 2.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')
  }
}

3.x 语法

在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。相反地,需要使用一个导入的 resolveComponent 方法:

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

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

22.插槽统一 非兼容

变化概览:

  • 此更改统一了 3.x 中的普通插槽和作用域插槽。
    • this.$slots 现在将插槽作为函数公开
    • 非兼容:移除 this.$scopedSlots

2.x 语法

当使用渲染函数时,即 h,2.x 用于在内容节点上定义 slot 数据 property。

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

此外,在引用作用域插槽时,可以使用以下方法引用它们:

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

3.x 语法

在 3.x 中,将插槽定义为当前节点的子对象:

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

当你需要以编程方式引用作用域插槽时,它们现在被统一到 $slots 选项中。

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

// 3.x 语法
this.$slots.header()

迁移策略

大部分更改已经在 2.6 中发布。因此,迁移可以一步到位:

  • 在 3.x 中,将所有 this.$scopedSlots 替换为 this.$slots。
  • 将所有 this.$slots.mySlot 替换为 this.$slots.mySlot()。

23. 过渡的 class 名更改 非兼容

变化概览

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。

2.x 语法

在 v2.1.8 版本之前,为过渡指令提供了两个过渡类名对应初始和激活状态。

在 v2.1.8 版本中,引入 v-enter-to 来定义 enter 或 leave 变换之间的过渡动画插帧,为了向下兼容,并没有变动 v-enter 类名:

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

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

3.x 语法

为了更加明确易读,我们现在将这些初始状态重命名为:

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

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

现在,这些状态之间的区别就清晰多了。

<transition> 组件相关属性名也发生了变化:

  • leave-class 已经被重命名为 leave-from-class (在渲染函数或 JSX 中可以写为:leaveFromClass)
  • enter-class 已经被重命名为 enter-from-class (在渲染函数或 JSX 中可以写为:enterFromClass)

24.Transition Group 根元素 非兼容

变化概览:

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

2.x 语法

在 Vue 2 中,<transition-group> 像其它自定义组件一样,需要一个根元素。默认的根元素是一个 <span>,但可以通过 tag prop 定制。

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

3.x 语法

在 Vue 3 中,我们有了片段的支持,因此组件不再需要根节点。所以,<transition-group> 默认不再渲染根节点。

  • 如果像上面的示例一样,已经在 Vue 2 代码中定义了 tag prop,那么一切都会和之前一样
  • 如果没有定义 tag prop,而且样式或其它行为依赖于 <span> 根元素的存在才能正常工作,那么只需将 tag="span" 添加到 <transition-group>:
<transition-group tag="span">
  <!-- -->
</transition-group>

25.移除 v-on.native 修饰符 非兼容

变化概览:

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

2.x 语法

默认情况下,传递给带有 v-on 的组件的事件监听器只有通过 this.$emit 才能触发。要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符:

<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

3.x 语法

v-on 的 .native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。

因此,对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)。

<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>

MyComponent.vue

<script>
  export default {
    emits: ['close']
  }
</script>

26.v-model 非兼容

变化概览:

  • 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
    • prop:value -> modelValue;
    • event:input -> update:modelValue;
  • 非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可用 v-model 作为代替;
  • 新增:现在可以在同一个组件上使用多个 v-model 进行双向绑定;
  • 新增:现在可以自定义 v-model 修饰符。

介绍

在 Vue 2.0 发布后,开发者使用 v-model 指令必须使用为 value 的 prop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind.sync。此外,由于v-model 和 value 之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。

在 Vue 2.2 中,我们引入了 model 组件选项,允许组件自定义用于 v-model 的 prop 和事件。但是,这仍然只允许在组件上使用一个 model。

在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model 指令时的混淆并且在使用 v-model 指令时可以更加灵活。

2.x 语法

在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 和 input 事件:

<ChildComponent v-model="pageTitle" />

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

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

如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent 组件中添加 model 选项:

<!-- ParentComponent.vue -->

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

export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    // 这将允许 `value` 属性用于其他用途
    value: String,
    // 使用 `title` 代替 `value` 作为 model 的 prop
    title: {
      type: String,
      default: 'Default title'
    }
  }
}

所以,在这个例子中 v-model 是以下的简写:

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

使用 v-bind.sync

在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model 绑定 prop 的情况)。为此,我们建议使用 update:myPropName 抛出事件。例如,对于在上一个示例中带有 title prop 的 ChildComponent,我们可以通过下面的方式将分配新 value 的意图传达给父级:

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

如果需要的话,父级可以监听该事件并更新本地 data property。例如:

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

为了方便起见,我们可以使用 .sync 修饰符来缩写,如下所示:

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

3.x 语法

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:

<ChildComponent v-model="pageTitle" />

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

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

v-model 参数

若需要更改 model 名称,而不是更改组件内的 model 选项,那么现在我们可以将一个 argument 传递给 model:

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

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

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

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

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

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

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

v-model 修饰符

除了像 .trim 这样的 2.x 硬编码的 v-model 修饰符外,现在 3.x 还支持自定义修饰符:

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

27.v-if 与 v-for 的优先级对比 非兼容

变化概览

  • 非兼容:两者作用于同一个元素上时,v-if 会拥有比 v-for 更高的优先级。

介绍

Vue.js 中使用最多的两个指令就是 v-if 和 v-for,因此开发者们可能会想要同时使用它们。虽然不建议这样做,但有时确实是必须的,于是我们想提供有关其工作方式的指南。

2.x 语法

2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。

3.x 语法

3.x 版本中 v-if 总是优先于 v-for 生效。

28.v-bind 合并行为 非兼容

变化概览:

  • 不兼容:v-bind 的绑定顺序会影响渲染结果

介绍

在元素上动态绑定 attribute 时,常见的场景是在一个元素中同时使用 v-bind="object" 语法和单独的 property。然而,这就引出了关于合并的优先级的问题。

2.x 语法

在 2.x,如果一个元素同时定义了 v-bind="object" 和一个相同的单独的 property,那么这个单独的 property 总是会覆盖 object 中的绑定。

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

3.x 语法

在 3.x,如果一个元素同时定义了 v-bind="object" 和一个相同的单独的 property,那么声明绑定的顺序决定了它们如何合并。换句话说,相对于假设开发者总是希望单独的 property 覆盖 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>

29.Watch on Arrays 非兼容

变化概览:

  • 非兼容: 当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组改变时触发回调,必须指定 deep 选项。

3.x 语法

当使用 watch 选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组改变时 watch 回调将不再被触发。要想在数组改变时触发 watch 回调,必须指定 deep 选项。

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

最后:

本笔记主要基于官方文档 迁移策略 汇总而来。如有理解出入,请以官方文档为主。建议您以官方文档为主,本文为辅。这样您可以“以自己为主”审视的阅读,从而不被我的观点带偏

分享下自己整理的部分知识点文章链接