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

u=706351709,1488187264&fm=26&gp=0.jpeg

1.v-for 中的 Ref 数组 非兼容

在 Vue 2 中,在 v-for 里使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。javascript

在 Vue 3 中,这样的用法将再也不在 $ref 中自动建立数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):html

2.x 语法

<template>
  <div> <div v-for="item in 10" :key="item" ref="liRef"></div> </div>
</template>
<script>
export default {
  name: 'Vue2Demo',
  data() {
    return {}
  },
  mounted() {
    console.log('refs', this.$refs.liRef)
    // vue2时,输出: refs (10) [div, div, div, div, div, div, div, div, div, div]
    // vue3时, 输出  refs <div>10<div>  (不会建立数组)
  }
}
</script>
复制代码

3.x 语法

  • 结合选项式 API:
<template>
  <div> <div v-for="item in 10" :key="item" :ref="setItemRef"> {{item}} </div> </div>
</template>
<script> export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { if (el) { this.itemRefs.push(el) } } }, mounted() { console.log('结合选项式 API:',this.itemRefs) } } </script>
复制代码
  • 结合组合式 API:
<template>
  <div v-for="item in 10" :ref="setItemRef"> {{item}} </div>
</template>

<script> import { defineComponent,onMounted} from 'vue' export default defineComponent({ setup() { let itemRefs = []; const setItemRef = el => { if (el) { itemRefs.push(el) } } onMounted(() => { console.log('结合组合式 API:',itemRefs); }) return { setItemRef } } }) </script>
复制代码

注意:前端

  • itemRefs 没必要是数组:它也能够是一个对象,其 ref 会经过迭代的 key 被设置。
  • 若是须要,itemRef 也能够是响应式的且能够被监听。

2. 异步组件 新增

变化概览:vue

  • 新的 defineAsyncComponent 助手方法,用于显式地定义异步组件
  • component 选项重命名为 loader
  • Loader 函数自己再也不接收 resolve 和 reject 参数,且必须返回一个 Promise

2.x 语法

异步组件是经过将组件定义为返回 Promise 的函数来建立的java

const AsyncComponent = () => import('@/components/AsyncComponent')

复制代码

或者,对于带有选项的更高阶的组件语法:node

const AsyncComponent = () => ({
  component: () => import('@/components/AsyncComponent'),
  delay: 200,
  timeout: 300,
  error: ErrorComponent,
  loading: LoadingComponent
})
复制代码

3.x 语法

在 Vue 3 中,因为函数式组件被定义为纯函数,所以异步组件的定义须要经过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:react

import { defineAsyncComponent } from 'vue'
import ErrorComponent from '@/components/ErrorComponent';
import LoadingComponent from '@/components/LoadingComponent';

// 不带选项的异步组件
const  AsyncComponent = defineAsyncComponent(() => import('@/components/AsyncComponent'))

// 带选项的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  // 2.x 里的component 选项改成 loader,以便准确地传达不能直接提供组件定义的信息。loader 函数再也不接收 resolve 和 reject 参数,且必须始终返回 Promise。 
  loader: () => import('@/components/AsyncComponent'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})
复制代码

3.attribute 强制行为 非兼容

前置知识:

  • HTML 属性
  • 布尔属性与枚举属性的区别
  • 内容属性与IDL属性的区别

布尔属性

  • 布尔属性有个特色:当声明了这个属性时,其值为 true;而未声明时,其值为 false。
  • HTML5 定义了布尔值属性容许的取值:若是属性存在,其值必须是一个空字符串(即该属性的值未分配),或者是一个大小写无关的 ASCII 字符串,该字符串与属性名严格相同,先后都没有空格。——摘自《MDN: Web 开发技术>HTML(超文本标记语言)>HTML 属性参考>布尔值属性
<div itemscope> This is valid HTML but invalid XML. </div>
<div itemscope=itemscope> This is also valid HTML but invalid XML. </div>
<div itemscope=""> This is valid HTML and also valid XML. </div>
<div itemscope="itemscope"> This is also valid HTML and XML, but perhaps a bit verbose. </div>
复制代码

上面四种写法是等效的。因此,布尔值属性不能取值为 “true” 和 “false”。若是须要表示 false 值,布尔值属性须要整个忽略不写。webpack

枚举属性

  • 枚举属性,顾名思义,就是取值是一个由若干关键词组成的枚举集合。例如 input 元素的 autocomplete 属性,这个属性可取值为 username、email、country、tel、url 等等。
  • 须要注意的是有些枚举属性只接受两个枚举值:true和false。并且,空字符串 或者 不给属性赋值 都等于true。下面写法都表明true
<div contenteditable>An editable item</div>
<div contenteditable="">An editable item</div>
<div contenteditable="true">An editable item</div>
复制代码

下面写法都表明falseweb

<div contenteditable="false">An editable item</div>
<div contenteditable="abcdefg">An editable item</div>
<div>An editable item</div>
复制代码

其余属性

除开上面两种属性分类,其他的属性能够归类于常规属性了。面试

内容属性 和 IDL(接口描述语言)属性:

HTML 中,属性还有 内容属性 和 IDL属性 说法。注意,这两种属性,并非对标签属性的划分。他们只是属性不一样地方的不一样描述和写法而已。

内容属性 接收的值都是字符串。编写 HTML 时,直接写在标签中的就是内容属性。此外,内容属性还能够经过 JS 的 setAttribute() 来设置。

<div contenteditable>An editable item</div>

input.setAttribute('type', 'text');
input.getAttribute('type');

而 IDL属性 是一个 JavaScript 属性(property),是 DOM 提供给 JS 的真正属性。经过 . 运算符来设置,且只接收正确类型的值。若是接收值的类型不正确,会自动转化成正确的类型。

input.type = 'password';
复制代码

MDN: Web 开发技术>HTML(超文本标记语言)>HTML 属性参考

变化概览:

  • 删除枚举 attribute 的内部概念,并将这些 attribute 视为普通的非布尔 attribute
  • 重大改变:若是值为布尔值,则再也不删除 attribute false。相反,它被设置为 attr=“false”。移除 attribute,应该使用 null 或者 undefined。

2.x 语法

  • 对于某些属性/元素对,Vue 采用 IDL 属性 形式处理:如 value of <input>, <select>, <progress>, 等等.
  • 对于 布尔值属性 和 xlinks,Vue 经过判断 是不是falsy(undefined、 null、false)值来决定添加或是删除属性。
  • 对于 枚举属性, Vue 强制转化为字符串。
  • 对于其余(普通非布尔)属性,若是传递过来的值是 falsy 值则删除,不然直接添加

下表描述了 Vue 如何使用普通非布尔 attribute 强制“枚举 attribute”:

v-bind表达式 普通非布尔属性:foo 枚举属性:draggable
:attr="null" / draggable="false"
:attr="undefined" / /
:attr="true" foo="true" draggable="true"
:attr="false" / draggable="false"
:attr="0" foo="0" draggable="true"
attr="" foo="" draggable="true"
attr="foo" foo="foo" draggable="true"
attr foo="" draggable="true"

/: 移除

从上面的对照表能够看出,二者的表现是不一致。这样会形成使用时的不便。

3.x 语法

在 Vue 3.x 中移除了枚举属性的概念,统一将他们视为普通非布尔属性。这样作的好处:

  • 消除了普通非布尔属性和枚举属性表现形式的不一致(换而言之,在 Vue 3.x 中,只存在非布尔属性和布尔属性)
  • 意味着能够对 枚举属性 使用除 true 和 false 之外的值,甚至是未使用的关键字。

此外,对于非布尔属性,若是传递的值是false,Vue 将再也不会删除属性了,而是强制转化为字符串'false'。

上面那张表格,在 Vue 3.x 中的表现则变成:

v-bind表达式 普通非布尔属性:foo 枚举属性:draggable
:attr="null" / /
:attr="undefined" / /
:attr="true" foo="true" draggable="true"
:attr="false" foo="false" draggable="false"
:attr="0" foo="0" draggable="0"
attr="" foo="" draggable=""
attr="foo" foo="foo" draggable="foo"
attr foo="" draggable=""

能够看到,普通非布尔属性 和 枚举属性 结果是一致的。

对于 非布尔属性,false 被强制转化为'false',再也不删除属性。因此,在 Vue 3.x 中,应该使用undefined和null来显式删除属性。

注意,布尔属性 表现并改变,和 Vue 2.x 保持一致。

Attribute v-bind value 2.x v-bind value 3.x HTML output
Vue 2.x 中的枚举属性,如: contenteditable, draggable and spellcheck. undefined, false undefined, null removed
true, 'true', '', 1, 'foo' true, 'true' "true"
null, 'false' false, 'false' "false"
Vue 2.x 中的普通非布尔属性,如:aria-checked, tabindex, alt, etc. undefined, null, false undefined, null removed
'false' false, 'false' "false"
布尔属性:required、disabled、readonly false、null、undefined false、null、undefined removed

实际代码测试

<div style="width: 500px">
  非枚举非布尔属性:true:<input type="text" :foo="true" />
  非枚举非布尔属性:false:<input type="text" :foo="false" />
  非枚举非布尔属性:undefined:<input type="text" :foo="undefined" />
  非枚举非布尔属性:null:<input type="text" :foo="null" />
  非枚举非布尔属性:0:<input type="text" :foo="0" />

  <hr />
  枚举属性:true:<input type="text" :spellcheck="true" />
  枚举属性:false:<input type="text" :spellcheck="false" />
  枚举属性:undefined:<input type="text" :spellcheck="undefined" />
  枚举属性:null:<input type="text" :spellcheck="null" />
  枚举属性:0:<input type="text" :spellcheck="0" />

  <hr />
  布尔属性required:true:<input type="text" :required="true" />
  布尔属性required:false:<input type="text" :required="false" />
  布尔属性required:undefined:<input type="text" :required="undefined" />
  布尔属性required:null:<input type="text" :required="null" />
  布尔属性required:0:<input type="text" :required="0" />
</div>
复制代码

结果: image.png

4. $attrs包含class&style 非兼容

变化概览:

  • 如今 $attrs 包含传递给组件的全部 attribute,包括 class 和 style。

2.x 语法

在 Vue 2 的虚拟 DOM 实现中对 class 和 style attribute 有一些特殊处理。所以,它们不包含在 $attrs 中,而其它全部 attribute 都包含在其中。

在使用 inheritAttrs: false 时会产生反作用:

  • $attrs 中的 attribute 再也不自动添加到根元素中,而是由开发者决定在哪添加。
  • 可是 class 和 style 不属于 $attrs,仍然会应用到组件的根元素:
<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>
复制代码

……将生成如下 HTML:

<label class="my-class">
  <input type="text" id="my-id" />
</label>
复制代码

3.x 语法

$attrs 包含全部的 attribute,这使得把它们所有应用到另外一个元素上更加容易。那么上面的示例就会生成如下 HTML:

<label>
  <input type="text" id="my-id" class="my-class" />
</label>
复制代码

5.$children 移除

变化概览:

  • $children 实例 property 已从 Vue 3.0 中移除,再也不支持。

2.x 语法

在 2.x 中,开发者可使用 this.$children 直接访问当前实例的子组件:

<template>
  <div> <img alt="Vue logo" src="./assets/logo.png"> <my-button>Change logo</my-button> </div> </template>

<script> import MyButton from './MyButton' export default { components: { MyButton }, mounted() { console.log(this.$children) // [VueComponent] } } </script>
复制代码

3.x 语法

在 3.x 中,$children property 已移除,再也不支持。若是你须要访问子组件实例,咱们建议使用 $refs。

6.自定义指令 非兼容

变化概览:

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

2.x 语法

在 Vue 2,自定义指令是经过使用下面列出的钩子来建立的,这些钩子都是可选的

  • bind - 指令绑定到元素后发生。只发生一次。
  • inserted - 元素插入父 DOM 后发生。
  • update - 当元素更新,但子元素还没有更新时,将调用此钩子。
  • componentUpdated - 一旦组件和子级被更新,就会调用这个钩子。
  • unbind - 一旦指令被移除,就会调用这个钩子。也只调用一次。

下面是一个例子:

<p v-highlight="'yellow'">高亮显示此文本亮黄色</p>
复制代码
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value
  }
})
复制代码

在这里,在这个元素的初始设置中,指令经过传递一个值来绑定样式,该值能够经过应用程序更新为不一样的值。

3.x 语法

然而,在 Vue 3 中,咱们为自定义指令建立了一个更具凝聚力的 API。正如你所看到的,它们与咱们的组件生命周期方法有很大的不一样,即便咱们正与相似的事件钩子,咱们如今把它们统一块儿来了:

  • created - 新的!在元素的 attribute 或事件侦听器应用以前调用。
  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate:新的!这是在元素自己更新以前调用的,很像组件生命周期钩子。
  • update → 移除!有太多的类似之处要更新,因此这是多余的,请改用 updated。
  • componentUpdated → updated
  • beforeUnmount:新的!与组件生命周期钩子相似,它将在卸载元素以前调用。
  • unbind -> unmounted

最终 API 以下:

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

生成的 API 能够这样使用,与前面的示例相同:

<p v-highlight="'yellow'">高亮显示此文本亮黄色</p>
复制代码
const app = Vue.createApp({})

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

7.自定义元素交互 非兼容

变化概览:

  • 非兼容:自定义元素白名单如今在模板编译期间执行,应该经过编译器选项而不是运行时配置来配置。
  • 非兼容:特定 is prop 用法仅限于保留的 <component> 标记。
  • 新增:有了新的 v-is 指令来支持 2.x 用例,其中在原生元素上使用了 v-is 来处理原生 HTML 解析限制。

自主定制元素

若是咱们想添加在 Vue 外部定义的自定义元素 (例如使用 Web 组件 API),咱们须要“指示”Vue 将其视为自定义元素。让咱们如下面的模板为例。

<plastic-button></plastic-button>
复制代码

2.x 语法

在 Vue 2.x 中,将标记做为自定义元素白名单是经过 Vue.config.ignoredElements:

// 这将使Vue忽略在Vue外部定义的自定义元素
// (例如:使用 Web Components API)

Vue.config.ignoredElements = ['plastic-button']
复制代码

3.x 语法

在 Vue 3.0 中,此检查在模板编译期间执行指示编译器将 <plastic-button> 视为自定义元素:

  • 若是使用生成步骤:将 isCustomElement 传递给 Vue 模板编译器,若是使用 vue-loader,则应经过 vue-loader 的 compilerOptions 选项传递:
// webpack 中的配置
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'
复制代码

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

定制内置元素

自定义元素规范提供了一种将自定义元素用做自定义内置模板的方法,方法是向内置元素添加 is 属性:

<button is="plastic-button">点击我!</button>
复制代码

Vue 对 is 特殊 prop 的使用是在模拟 native attribute 在浏览器中广泛可用以前的做用。可是,在 2.x 中,它被解释为渲染一个名为 plastic-button 的 Vue 组件,这将阻止上面提到的自定义内置元素的原生使用。

在 3.0 中,咱们仅将 Vue 对 is 属性的特殊处理限制到 <component> tag。

  • 在保留的 <component> tag 上使用时,它的行为将与 2.x 中彻底相同;

  • 在普通组件上使用时,它的行为将相似于普通 prop:

    <foo is="bar" />
    复制代码
    • 2.x 行为:渲染 bar 组件。
    • 3.x 行为:经过 is prop 渲染 foo 组件。
  • 在普通元素上使用时,它将做为 is 选项传递给 createElement 调用,并做为原生 attribute 渲染,这支持使用自定义的内置元素。

    <button is="plastic-button">点击我!</button>
    复制代码
    • 2.x 行为:渲染 plastic-button 组件。

    • 3.x 行为:经过回调渲染原生的 button。

      document.createElement('button', { is: 'plastic-button' })
      复制代码

v-is 用于 DOM 内模板解析解决方案

提示:本节仅影响直接在页面的 HTML 中写入 Vue 模板的状况。 在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如 <ul>,<ol>,<table> 和 <select> 对它们内部能够出现的元素有限制,和一些像 <li>,<tr>,和 <option> 只能出如今某些其余元素中。

2x 语法

在 Vue 2 中,咱们建议经过在原生 tag 上使用 is prop 来解决这些限制:

<table>
  <tr is="blog-post-row"></tr>
</table>
复制代码

3.x 语法

随着 is 的行为变化,咱们引入了一个新的指令 v-is,用于解决这些状况:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>
复制代码

v-is 函数像一个动态的 2.x :is 绑定——所以,要按注册名称渲染组件,其值应为 JavaScript 字符串文本:

<!-- 不正确,不会渲染任何内容 -->
<tr v-is="blog-post-row"></tr>

<!-- 正确 -->
<tr v-is="'blog-post-row'"></tr>
复制代码

8.Data 选项 非兼容

变化概览:

  • 非兼容:data 组件选项声明再也不接收纯 JavaScript object,而须要 function 声明。

当合并来自 mixin 或 extend 的多个 data 返回值时,如今是浅层次合并的而不是深层次合并的(只合并根级属性)。

2x 语法

在 2.x 中,开发者能够定义 data 选项是 object 或者是 function。

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

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

虽然这对于具备共享状态的根实例提供了一些便利,可是因为只有在根实例上才有可能,这致使了混乱。

3x 语法

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

使用上面的示例,代码只有一个可能的实现:

<script>
  import { createApp } from 'vue'

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

Mixin 合并行为变动

此外,当来自组件的 data() 及其 mixin 或 extends 基类被合并时,如今将浅层次执行合并:

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

在 Vue 2.x中,生成的 $data 是:

{
  user: {
    id: 2,
    name: 'Jack'
  }
}
复制代码

在 3.0 中,其结果将会是:

{
  user: {
    id: 2
  }
}
复制代码

9.emits Option 新增

变化概览: Vue 3如今提供了一个 emits 选项,相似于现有的 props 选项。此选项可用于定义组件能够发送给其父组件的事件。

2x 语法

在Vue 2中,你能够定义组件接收的props,但你不能声明它能够发出哪些事件:

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

3x 语法

与props相似,组件发出的事件如今能够用emits选项来定义:

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

该选项还接受一个对象,它容许开发人员为随触发事件传递的参数定义验证器,相似于props定义中的验证器。详情见

10.事件 API 非兼容

变化概览:

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

2.x 语法

在 2.x 中,Vue 实例可用于触发由事件触发 API 经过指令式方式添加的处理函数 ( o n on, off 和 $once)。这能够建立 event hub,用来建立在整个应用程序中可用的全局事件监听器:

// 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 被挂载,控制台中将显示一条消息
    }
  }
}
复制代码

3.x 语法

咱们从实例中彻底移除了 o n on、 off 和 o n c e 方法。 once 方法。 emit 仍然包含于现有的 API 中,由于它用于触发由父组件声明式添加的事件处理函数。

11.过滤器 移除

变化概览:

  • 从 Vue 3.0 开始,过滤器已删除,再也不支持。

2.x 语法

在 2.x,开发者可使用过滤器来处理通用文本格式。

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>

<script> export default { props: { accountBalance: { type: Number, required: true } }, filters: { currencyUSD(value) { return '$' + value } } } </script>
复制代码

虽然这看起来很方便,但它须要一个自定义语法,打破大括号内表达式是“只是 JavaScript”的假设,这不只有学习成本,并且有实现成本。

3.x 语法

在 3.x 中,过滤器已删除,再也不支持。相反地,咱们建议用方法调用或计算属性替换它们。

使用上面的例子,这里是一个如何实现它的例子。

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountInUSD }}</p>
</template>

<script> export default { props: { accountBalance: { type: Number, required: true } }, computed: { accountInUSD() { return '$' + this.accountBalance } } } </script>
复制代码

建议用计算属性或方法代替过滤器,而不是使用过滤器

全局过滤器

若是在应用中全局注册了过滤器,那么在每一个组件中用计算属性或方法调用来替换它可能就没那么方便了。

相反地,你能够经过全局属性在全部组件中使用它:

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

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

而后,你能够经过 $filters 对象修改全部的模板,像下面这样:

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
复制代码

注意,这种方式只能用于方法中,不能够在计算属性中使用,由于后者只有在单个组件的上下文中定义时才有意义。

12.片断 新增

变化概览:

  • Vue 3 如今正式支持了多根节点的组件,也就是片断!

2.x 语法

在 2.x 中,因为不支持多根节点组件,当开发者意外建立一个时会发出警告。为了修复这个问题,许多组件被包裹在一个 <div> 中。

<!-- Layout.vue -->
<template> <div> <header>...</header> <main>...</main> <footer>...</footer> </div> </template>
复制代码

3.x 语法

在 3.x 中,组件能够包含多个根节点!可是,这要求开发者显式定义 attribute 应该分布在哪里。

<!-- Layout.vue -->
<template> <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> </template>
复制代码

13.函数式组件 非兼容

变化概览:

  • 在 3.x 中,函数式组件 2.x 的性能提高能够忽略不计,所以咱们建议只使用有状态的组件
  • 函数式组件只能使用接收 props 和 context 的普通函数建立 (即:slots,attrs,emit)。
  • 非兼容变动:functional attribute 在单文件组件 (SFC) <template> 已被移除
  • 非兼容变动:{ functional: true } 选项在经过函数建立组件已被移除

介绍:

在 Vue 2 中,函数式组件有两个主要应用场景:

  • 做为性能优化,由于它们的初始化速度比有状态组件快得多
  • 返回多个根节点

然而,在 Vue 3 中,有状态组件的性能已经提升到能够忽略不计的程度。此外,有状态组件如今还包括返回多个根节点的能力。

所以,函数式组件剩下的惟一应用场景就是简单组件,好比建立动态标题的组件。不然,建议你像日常同样使用有状态组件。

2.x 语法

使用 组件,负责提供适当的标题 (即:h1,h2,h3,等等),在 2.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>
复制代码

3.x 语法

  • 经过函数建立组件

如今在 Vue 3 中,全部的函数式组件都是用普通函数建立的,换句话说,不须要定义 { functional: true } 组件选项。

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

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

使用前面提到的 <dynamic-heading> 组件的示例,下面是它如今的样子。

import { h } from 'vue'

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

DynamicHeading.props = ['level']

export default DynamicHeading
复制代码
  • 单文件组件 (SFC)

在 3.x 中,有状态组件和函数式组件之间的性能差别已经大大减小,而且在大多数用例中是微不足道的。所以,在 SFCs 上使用 functional 的开发人员的迁移路径是删除该 attribute,并将 props 的全部引用重命名为 $props,将 attrs 重命名为 $attrs。

使用以前的 <dynamic-heading> 示例,下面是它如今的样子。

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

<script> export default { props: ['level'] } </script>
复制代码

主要的区别在于:

  • functional attribute 在 <template> 中移除
  • listeners 如今做为 $attrs 的一部分传递,能够将其删除

14.全局 API 非兼容

Vue 2.x 有许多全局 API 和配置,这些 API 和配置能够全局改变 Vue 的行为。例如,要注册全局组件,可使用 Vue.component 这样的 API:

Vue.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
复制代码

相似地,使用全局指令的声明方式以下:

Vue.directive('focus', {
  inserted: el => el.focus()
})
复制代码

虽然这种声明方式很方便,但它也会致使一些问题。从技术上讲,Vue 2 没有“app”的概念,咱们定义的应用只是经过 new Vue() 建立的根 Vue 实例。从同一个 Vue 构造函数建立的每一个根实例共享相同的全局配置,所以:

  • 在测试期间,全局配置很容易意外地污染其余测试用例。用户须要仔细存储原始全局配置,并在每次测试后恢复 (例如重置 Vue.config.errorHandler)。有些 API 像 Vue.use 以及 Vue.mixin 甚至连恢复效果的方法都没有,这使得涉及插件的测试特别棘手。实际上,vue-test-utils 必须实现一个特殊的 API createLocalVue 来处理此问题:
    import { createLocalVue, mount } from '@vue/test-utils'
    
    // 建扩展的 `Vue` 构造函数
    const localVue = createLocalVue()
    
    // 在 “local” Vue构造函数上 “全局” 安装插件
    localVue.use(MyPlugin)
    
    // 经过 `localVue` 来挂载选项
    mount(Component, { localVue })
    复制代码
  • 全局配置使得在同一页面上的多个“app”之间共享同一个 Vue 副本很是困难,但全局配置不一样。
    // 这会影响两个根实例
    Vue.mixin({
      /* ... */
    })
    
    const app1 = new Vue({ el: '#app-1' })
    const app2 = new Vue({ el: '#app-2' })
    复制代码

为了不这些问题,在 Vue 3 中咱们引入...

一个新的全局 API:createApp

调用 createApp 返回一个应用实例,这是 Vue 3 中的新概念:

import { createApp } from 'vue'

const app = createApp({})
复制代码

若是你使用的是 Vue 的 CDN 构建,那么 createApp 是经过全局的 Vue 对象暴露的。

const { createApp } = Vue

const app = createApp({})
复制代码

应用实例暴露了 Vue 2 当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 如今都会移动到应用实例上,如下是当前 Vue2 全局 API 及其相应实例 API 的表

2.x 全局 API 3.x 实例 API (app)
Vue.config app.config
Vue.config.productionTip removed (见下方)
Vue.config.ignoredElements app.config.isCustomElement (见下方)
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use (见下方)
Vue.prototype app.config.globalProperties (见下方)

全部其余不全局改变行为的全局 API 如今被命名为 exports,文档见 全局 API Treeshaking

config.productionTip 移除

在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建) 时才会显示。

对于 ES 模块构建,因为它们是与 bundler 一块儿使用的,并且在大多数状况下,CLI 或样板已经正确地配置了生产环境,因此本技巧将再也不出现。

config.ignoredElements 替换为 config.isCustomElement

引入此配置选项的目的是支持原生自定义元素,所以重命名能够更好地传达它的功能,新选项还须要一个比旧的 string/RegExp 方法提供更多灵活性的函数:

// 以前
Vue.config.ignoredElements = ['my-el', /^ion-/]

// 以后
const app = createApp({})
app.config.isCustomElement = tag => tag.startsWith('ion-')
复制代码

在 Vue 3 中,元素是不是组件的检查已转移到模板编译阶段,所以只有在使用运行时编译器时才考虑此配置选项。若是你使用的是 runtime-only 版本 isCustomElement 必须经过 @vue/compiler-dom 在构建步骤替换——好比,经过 compilerOptions option in vue-loader。

  • 若是 config.isCustomElement 当使用仅运行时构建时时,将发出警告,指示用户在生成设置中传递该选项;
  • 这将是 Vue CLI 配置中新的顶层选项。

Vue.prototype 替换为 config.globalProperties

在 Vue 2 中, Vue.prototype 一般用于添加全部组件都能访问的 property。

在 Vue 3 等同于config.globalProperties。这些 property 将被复制到应用中做为实例化组件的一部分。

// 以前 - Vue 2
Vue.prototype.$http = () => {}
复制代码
// 以后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
复制代码

插件使用者须知

插件开发者一般使用 Vue.use。例如,官方的 vue-router 插件是如何在浏览器环境中自行安装的:

var inBrowser = typeof window !== 'undefined'
/* … */
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
复制代码

因为 use 全局 API 在 Vue 3 中再也不使用,此方法将中止工做并中止调用 Vue.use() 如今将触发警告,因而,开发者必须在应用程序实例上显式指定使用此插件:

const app = createApp(MyApp)
app.use(VueRouter)
复制代码

挂载 App 实例

使用 createApp(/* options */) 初始化后,应用实例 app 可用 app.mount(domTarget) 挂载根组件实例:

import { createApp } from 'vue'
import MyApp from './MyApp.vue'

const app = createApp(MyApp)
app.mount('#app')
复制代码

通过全部这些更改,咱们在指南开头的组件和指令将被改写为以下内容:

const app = createApp(MyApp)

app.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

app.directive('focus', {
  mounted: el => el.focus()
})

// 如今全部应用实例都挂载了,与其组件树一块儿,将具备相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境
app.mount('#app')
复制代码

Provide / Inject

与在 2.x 根实例中使用 provide 选项相似,Vue 3 应用实例还能够提供可由应用内的任何组件注入的依赖项:

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

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

使用 provide 在编写插件时很是有用,能够替代 globalProperties。

在应用之间共享配置

在应用之间共享配置 (如组件或指令) 的一种方法是建立工厂功能,以下所示:

import { createApp } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const createMyApp = options => {
  const app = createApp(options)
  app.directive('focus', /* ... */)

  return app
}

createMyApp(Foo).mount('#foo')
createMyApp(Bar).mount('#bar')
复制代码

如今,Foo 和 Bar 实例及其后代中均可以使用 focus 指令。

15.全局 API Treeshaking 非兼容

2.x 语法

若是你曾经在 Vue 中手动操做过 DOM,你可能会遇到如下模式:

import Vue from 'vue'

Vue.nextTick(() => {
  // 一些和DOM有关的东西
})
复制代码

或者,若是你一直在对涉及 async components 的应用程序进行单元测试,那么极可能你编写了如下内容:

import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // 执行一些DOM相关的任务

  await wrapper.vm.$nextTick()

  // 运行你的断言
})
复制代码

Vue.nextTick() 是一个全局的 API 直接暴露在单个 Vue 对象上。事实上,实例方法 $nextTick() 只是 Vue.nextTick() 的一个便利的包裹器,回调的 this 上下文自动绑定到当前实例上,以方便使用。

可是,若是你历来没有处理过手动的 DOM 操做,也没有在你的应用中使用或测试异步组件,怎么办?或者,无论出于什么缘由,你更喜欢使用老式的 window.setTimeout() 来代替呢?在这种状况下,nextTick() 的代码就会变成死代码--也就是说,写了代码但从未使用过。而死代码几乎不是一件好事,尤为是在咱们的客户端上下文中,每一行代码都很重要。

模块捆绑程序,如 webpack 支持 tree-shaking,这是“死代码消除”的一个花哨术语。不幸的是,因为代码是如何在之前的 Vue 版本中编写的,全局 API Vue.nextTick() 不可摇动,将包含在最终捆绑中无论它们实际在哪里使用。

3.x 语法

在 Vue 3 中,全局和内部 API 都通过了重构,并考虑到了 tree-shaking 的支持。所以,全局 API 如今只能做为 ES 模块构建的命名导出进行访问。例如,咱们以前的片断如今应该以下所示:

import { nextTick } from 'vue'

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

import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'
import { nextTick } from 'vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // 执行一些DOM相关的任务

  await nextTick()

  // 运行你的断言
})
复制代码

直接调用 Vue.nextTick() 将致使臭名昭著的 undefined is not a function 错误。

经过这一更改,若是模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的全局 api 将从最终捆绑包中消除,从而得到最佳的文件大小。

受影响的 API

Vue 2.x 中的这些全局 API 受此更改的影响:

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅全构建)
  • Vue.set (仅兼容构建)
  • Vue.delete (仅兼容构建)

内部帮助器

除了公共 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]])])
}

复制代码

这实际上意味着只有在应用程序实际使用了 Transition 组件时才会导入它。换句话说,若是应用程序没有任何 Transition 组件,那么支持此功能的代码将不会出如今最终的捆绑包中。

随着全局 tree-shaking,用户只需为他们实际使用的功能“付费”,更好的是,知道了可选特性不会增长不使用它们的应用程序的捆绑包大小,框架大小在未来已经再也不是其余核心功能的考虑因素了,若是有的话。

以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括全部特性,并暴露 Vue 全局变量上的全部内容 (编译器将生成适当的输出,才得以使用全局外的 api 而不是导入)。

插件中的用法

若是你的插件依赖受影响的 Vue 2.x 全局 API,例如:

const plugin = {
  install: Vue => {
    Vue.nextTick(() => {
      // ...
    })
  }
}
复制代码

在 Vue 3 中,必须显式导入:

import { nextTick } from 'vue'

const plugin = {
  install: app => {
    nextTick(() => {
      // ...
    })
  }
}
复制代码

若是使用 webpack 这样的模块捆绑包,这可能会致使 Vue 的源代码绑定到插件中,并且一般状况下,这并非你所指望的。防止这种状况发生的一种常见作法是配置模块绑定器以将 Vue 从最终捆绑中排除。对于 webpack,你可使用 externals 配置选项:

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

这将告诉 webpack 将 Vue 模块视为一个外部库,而不是捆绑它。

若是你选择的模块绑定器刚好是 Rollup,你基本上能够无偿得到相同的效果,由于默认状况下,Rollup 会将绝对模块 id (在咱们的例子中为 'vue') 做为外部依赖项,而不会将它们包含在最终的 bundle 中。可是在绑按期间,它可能会抛出一个“将 vue 做为外部依赖”警告,可以使用 external 选项抑制该警告:

// rollup.config.js
export default {
  /*...*/
  external: ['vue']
}
复制代码

最后:

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

分享下本身整理的部分知识点文章连接

相关文章
相关标签/搜索