阅读 953

是时候从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 时,这种行为会变得不明确且效率低下。

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

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. 异步组件 新增

变化概览:

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

2.x 语法

异步组件是通过将组件定义为返回 Promise 的函数来创建的

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

复制代码

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

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

3.x 语法

在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:

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 值,布尔值属性需要整个忽略不写。

枚举属性

  • 枚举属性,顾名思义,就是取值是一个由若干关键词组成的枚举集合。例如 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>
复制代码

下面写法都代表false

<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"
attrfoo=""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"
attrfoo=""draggable=""

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

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

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

Attributev-bind value 2.xv-bind value 3.xHTML output
Vue 2.x 中的枚举属性,如: contenteditable, draggable and spellcheck.undefined, falseundefined, nullremoved
true, 'true', '', 1, 'foo'true, 'true'"true"
null, 'false'false, 'false'"false"
Vue 2.x 中的普通非布尔属性,如:aria-checked, tabindex, alt, etc.undefined, null, falseundefined, nullremoved
'false'false, 'false'"false"
布尔属性:required、disabled、readonlyfalse、null、undefinedfalse、null、undefinedremoved

实际代码测试

<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 通过指令式方式添加的处理函数 (onon,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 语法

我们从实例中完全移除了 onon、off 和 once方法。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 全局 API3.x 实例 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 (见下方)

所有其他不全局改变行为的全局 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']
}
复制代码

最后:

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

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

文章分类
前端
文章标签