Vue3学习——迁移指南

848 阅读7分钟

注:其中某些(...)和(???)是作者待补充(没咋看明白的)的

迁移指南

1. 要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上

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

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

2. defineAsyncComponent

3. $attrs 现在包含了所有传递给组件的 attribute,包括 class 和 style

4. $children 实例 property 已从 Vue 3.0 中移除,不再支持

如果你需要访问子组件实例,我们建议使用 $refs

5. 自定义指令

为自定义指令创建了一个更具凝聚力的 API !!

  • ==created== - 新增!在元素的 attribute 或事件监听器被应用之前调用。
  • bind → ==beforeMount==
  • inserted → ==mounted==
  • ==beforeUpdate==:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
  • update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated。
  • componentUpdated → ==updated==
  • ==beforeUnmount==:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
  • unbind -> ==unmounted==

最终的 API 如下:

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

在 Vue 3 中,实例现在是 binding 参数的一部分:

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

6. 与自定义元素的互操作性(???)

7. Data选项

data 选项已标准化为只接受返回 object 的 function

<script>
  import { createApp } from 'vue'

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

Mixin 合并行为变更

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

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

在 3.0 中,其结果将会是:

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

8. emits选项

和 prop 类似,现在可以通过 emits 选项来定义组件可触发的事件:

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

9. 事件订阅用法

==我们从实例中完全移除了 onon、off 和 once方法。once 方法。emit 仍然包含于现有的 API 中,因为它用于触发由父组件声明式添加的事件处理函数。==

事件总线模式

事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或tiny-emitter

// eventBus.js
import emitter from 'tiny-emitter/instance'

export default {
  $on: (...args) => emitter.on(...args),
  $once: (...args) => emitter.once(...args),
  $off: (...args) => emitter.off(...args),
  $emit: (...args) => emitter.emit(...args),
}

有多种事件总线的替代方案:

  • Prop 和事件应该是父子组件之间沟通的首选。兄弟节点可以通过它们的父节点通信。
  • Provide 和 inject 允许一个组件与它的插槽内容进行通信。这对于总是一起使用的紧密耦合的组件非常有用。
  • provide/inject 也能够用于组件之间的远距离通信。它可以帮助避免“prop 逐级透传”,即 prop 需要通过许多层级的组件传递下去,但这些组件本身可能并不需要那些 prop。
  • Prop 逐级透传也可以通过重构以使用插槽来避免。如果一个中间组件不需要某些 prop,那么表明它可能存在关注点分离的问题。在该类组件中使用 slot 可以允许父节点直接为它创建内容,因此 prop 可以被直接传递而不需要中间组件的参与。
  • 全局状态管理,比如 Vuex。

10. 过滤器 filters 被移除

我们建议用方法调用或计算属性来替换它们。

全局过滤器

你可以通过全局属性(globalProperties)以让它能够被所有组件使用到:

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

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

11. Vue 3 现在正式支持了多根节点的组件,也就是片段!(...)

12. 函数式组件

Vue 2.X 版本

export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}

单文件组件(Vue 2.X 版本)

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

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

通过函数创建组件

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

它们将接收两个参数:propscontextcontext 参数是一个对象,包含组件的 attrs、slots 和 emit property。

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

Vue 3.X 版本

import { h } from 'vue'

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

DynamicHeading.props = ['level']

export default DynamicHeading

单文件组件(Vue 3.X 版本)

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

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

主要的区别在于:

  1. 从 template 中移除 functional attribute
  2. ==listeners 现在作为 $attrs 的一部分传递==,可以将其删除

13. 全局 API

一个新的全局 API —— createApp

import { createApp } from 'vue'

const app = createApp({})
  • 挂载根实例
import { createApp } from 'vue'
import MyApp from './MyApp.vue'

const app = createApp(MyApp)
app.mount('#app')
2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTip移除 (见下方)
Vue.config.ignoredElementsapp.config.compilerOptions.isCustomElement (见下方)
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.use app.use(见下方)
Vue.prototypeapp.config.globalProperties (见下方)
Vue.extend移除 (见下方)

全局API Treeshaking

  • ==Vue.prototype 替换为 config.globalProperties==
// 之前 - Vue 2
Vue.prototype.$http = () => {}

// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
  • Vue.extend 移除
// 之前 - Vue 2

// 创建构造器
const Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data() {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建一个 Profile 的实例,并将它挂载到一个元素上
new Profile().$mount('#mount-point')

// 之后 - Vue 3
const Profile = {
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data() {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
}
Vue.createApp(Profile).mount('#mount-point')
  • 组件继承

在 Vue 3 中,我们强烈建议==使用 组合式 API 来替代继承与 mixin==。如果因为某种原因仍然需要使用组件继承,你可以使用 extends 选项 来代替 Vue.extend。

  • 插件的使用

由于 use 全局 API 在 Vue 3 中已无法使用,因此此方法将无法正常工作,并且调用 Vue.use() 现在将触发一个警告。取而代之的是,开发者必须在应用实例上显式指定使用此插件:

const app = createApp(MyApp)
app.use(VueRouter)
  • Provide / Inject

Vue 3 应用实例也提供了可被应用内任意组件注入的依赖项:

// 在入口中
app.provide('guide', 'Vue 3 Guide')

// 在子组件中
export default {
  inject: {
    book: {
      from: 'guide'
    }
  },
  template: `<div>{{ book }}</div>`
}

==在编写插件时使用 provide 将尤其有用,可以替代 globalProperties。==

14. Tree Shaking (...)

如 webpack 这样的模块打包工具支持 tree-shaking,这是表达“消除死代码”的一个花哨术语。遗憾的是,由于之前的 Vue 版本中的代码编写方式,如 Vue.nextTick() 这样的全局 API 是不支持 tree-shake 的,不管它们实际上是否被使用了,都会被包含在最终的打包产物中。

在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,对于 ES 模块构建版本来说,全局 API 现在通过具名导出进行访问。

15. 内联模板(???)

16. Key

==对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。(本来我也没加过....)==

结合 template v-for

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

<!-- Vue 2.x -->
<template v-for="item in list">
  <div :key="'heading-' + item.id">...</div>
  <span :key="'content-' + 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>

17. 按键修饰符

从 KeyboardEvent.keyCode ==已被废弃==开始

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

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

现在建议对任何要用作修饰符的键使用 ==kebab-cased (短横线)== 名称。

<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">

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

<!-- 对于某些标点符号键 -->
<input v-on:keypress.,="commaPress">

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

18. 挂载API变化 (...)

19. propsData 移除

在 2.x 中,我们可以在创建 Vue 实例的时候传入 prop:

const Comp = Vue.extend({
  props: ['username'],
  template: '<div>{{ username }}</div>'
})

new Comp({
  propsData: {
    username: 'Evan'
  }
})

如果你需要在实例创建时向根组件传入 prop,你应该使用 createApp 的第二个参数:

const app = createApp(
  {
    props: ['username'],
    template: '<div>{{ username }}</div>'
  },
  { username: 'Evan' }
)

20. 在 prop 的默认函数中访问 this (???)

21. 渲染函数 API

  • 渲染函数参数
<!-- h 函数现在是全局导入的,而不是作为参数自动传递。-->
// Vue 3 渲染函数示例
import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}
  • 渲染函数签名更改

由于 ==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
      )
  }
}
  • 注册组件
// 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 中,由于 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.scopedSlots替换为this.scopedSlots 替换为 this.slots。==

23. Suspense (实验性)

24. 过渡

  • 过渡的 class 名更改

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

  • Transition 作为根节点

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

  • Transition Group 根元素

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

25. 移除 v-on.native 修饰符

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

26. v-model

当在组件上使用v-model时

Vue 2.x

==在组件上使用 v-model 相当于绑定 value prop 并触发 input 事件==

<ChildComponent v-model="pageTitle" />

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

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

如果想要更改 prop 或事件名称,则需要在 ChildComponent 组件中添加 model 选项:

// ChildComponent.vue

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

Vue 3.x

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

<ChildComponent v-model="pageTitle" />

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

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

若需要更改 model 的名称,现在我们可以为 v-model 传递一个参数,以作为组件内 model 选项的替代:

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

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

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

image

这也可以作为 .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"
/>

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

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

28. v-bind 合并行为

后面定义的覆盖前面的:

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

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

29. VNode 生命周期事件(....)

30. 侦听数组

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

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