组件相关
函数式组件
functional attribute 在单文件组件 (SFC) <template>
已被移除
{ functional: true } 选项在通过函数创建组件已被移除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
export default { functional: true, props: ['level'], render(h, { props, data, children }) { return h(`h${props.level}`, data, children) } }
<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,slots,和 emit property。
此外,现在不是在 render 函数中隐式提供 h,而是全局导入 h。
使用前面提到的 组件的示例,下面是它现在的样子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { h } from 'vue' const DynamicHeading = (props, context) => { return h(`h${props.level}`, context.attrs, context.slots) } DynamicHeading.props = ['level'] export default DynamicHeading
<template> <component v-bind:is="`h${$props.level}`" v-bind="$attrs" /> </template>
<script> export default { props: ['level'] } </script>
|
主要区别在于
1 2
| functional attribute 在 <template> 中移除 listeners 现在作为 $attrs 的一部分传递,可以将其删除
|
异步组件的写法与 defineAsyncComponent 方法
现在使用 defineAsyncComponent 助手方法,用于显示的定义异步组件
component 选项重命名为 loader
Loader 函数本身不再接受 resolve 和 rejuct 参数,必须返回一个 Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
const asyncPage = () => import('./NextPage.vue')
const asyncPage = { component: () => import('./NextPage.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent }
在vue3.x中,需要使用defineAsyncComponent来定义 import{ defineAsyncComponent } from 'vue' import ErrorComponent from './components/ErrorComponent.vue' import LoadingComponent from './components/LoadingComponent.vue'
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
constasyncPageWithOptions = defineAsyncCopmonent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, errorComponent: ErrorComponent, LoadingComponent: LoadingComponent })
|
loader 函数不再接收 resolve 和 reject 参数,且必须始终返回 Promise
1 2 3 4 5 6
| const oldAsyncComponent = (resolve, reject) => {};
const asyncComponent = defineAsyncComponent( () => new Promise((resolve, reject) => {}) );
|
组件事件需要在 emits 选项中声明
vue3 中现在提供了一个 emits 选项,类似 props 选项
此选项可以用于定义组件向其父对象发出的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div> <p>{{ text }}</p> <button v-on:click="$emit('accepted')">OK</button> </div> </template> <script> export default { props: ["text"], }; </script>
<template> <div> <p>{{ text }}</p> <button v-on:click="$emit('accepted')">OK</button> </div> </template> <script> export default { props: ["text"], emits: ["accepted"], }; </script>
|
全局 API 变化
Vue2 的全局 Api
vue2 的全局 Api 可以全局改变 vue 的行为,这种操作容易意外污染其他测试用例
1 2 3 4 5 6 7
| import { createLocalVue, mount } from "@vue/test-utils";
const localVue = createLocalVue();
localVue.use(Myplugin);
mount(Component, { localVue });
|
全局配置会使同一个页面上的多个 app 之间共享一个 Vue 副本非常困难
1 2 3 4 5 6 7
| Vue.mixin({ });
const app1 = new Vue({ el: "#app1" }); const app2 = new Vue({ el: "#app2" });
|
Vue3 中新的全局 Api:createApp
createApp 返回了一个应用实例,
1 2 3 4 5 6
| import { createApp } from "vue"; const app = createApp({});
const { createApp } = Vue; const app = createApp({});
|
Vue3 中对比 Vue2 全局 Api 的变化
2.x 全局 Api |
3.x 实例(app)Api |
Vue.cofing |
app.config |
Vue.config.productionTip |
* 移除 |
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
config.productionTip 移除
在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建) 时才会显示。
对于 ES 模块构建,由于它们是与 bundler 一起使用的,而且在大多数情况下,CLI 或样板已经正确地配置了生产环境,所以本技巧将不再出现。
config.ignoredElements 替换为 config.isCustomElement
引入此配置项目目的是为了支持原生自定义元素,因此重命名可以更好的传达它的功能,新选项还需要一个比使用 String/RegExp 跟灵活的函数
1 2 3 4 5
| Vue.config.ignoredElements = ["my-el", /^ion-/];
const app = createApp({}); app.config.isCustomElement = (tag) => tag.startsWith("ion-");
|
1 2 3 4
| 在 Vue 3 中,元素是否是组件的检查已转移到模板编译阶段,因此只有在使用运行时编译器时才考虑此配置选项。如果你使用的是 runtime-only 版本 isCustomElement 必须通过 @vue/compiler-dom 在构建步骤替换——比如,通过 compilerOptions option in vue-loader。
如果 config.isCustomElement 当使用仅运行时构建时时,将发出警告,指示用户在生成设置中传递该选项; 这将是 Vue CLI 配置中新的顶层选项。
|
Vue.prototype 替换为 config.globalProperties
在 Vue2 中,Vue.prototype 通常用于添加所有组件都能访问的 property
在 Vue3 等同于 config.globalProperties 这些 property 将被复制到应用中作为实例化组件的一部分
1 2 3 4 5 6 7
| Vue.prototype.$http = () => {};
const app = createApp({}); app.config.globalProperties.$http = () => {};
|
插件的使用
插件开发者通常使用 vue.use,例如官方的 vue-router 是如何在浏览器环境中自行安装的
1 2 3 4 5
| var inBrowser = typeof window !== "undefined";
if (inBrowser && window.Vue) { window.Vue.use(VueRouter); }
|
现在 use 全局 Api 不再使用,所以需要手动指定使用此插件
1 2
| const app = createApp(MyApp); app.use(VueRouter);
|
挂载 App 实例
使用 createApp 初始化之后,应用实例 app 可使用 app.mount(domTarget)挂在组件实例,
经过以上更改,完整的写法将会改写为以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { createApp } from "vue"; import MyApp from "./MyApp.vue";
const app = createApp(MyApp); app.componenet("button-counter", { data: () => ({ count: 0, }), template: `<button @click="count++">Clicked {{ count }} times.</button>`, });
app.directive("focus", { mounted: (el) => el.focus(), }); app.mount("#app");
|
Provide/inject
与 2.x 跟实例中使用 provide 选项类似,Vue3 应用实例还可以提供可由应用内的任何组件注入的依赖项
1 2 3 4 5 6 7 8 9 10 11 12
| app.provide("guide", "Vue 3 Guide");
export default { inject: { book: { from: "guide", }, }, template: `<div>{{ book }}</div>`, };
|
应用之间共享配置
要在应用之间共享配置,如组件或指令的一种方法时创建组件工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { createApp } from "vue"; import F00 from "./Foo.vue"; import Bar from "./Bar.vue";
const createMyApp = (options) => { const app = createApp(options); app.directive("focus" );
return app; };
createApp(Foo).mount("#foo"); createApp(Bar).mount("#bar");
|
支持 Tree-shaking 的影响
2.x 不支持 tree-shaking
tree-shaking,即死代码消除,但是由于 2.x 的一些 api,如 Vue.nextTick()方法,即使不被使用,也会被最终打包
3.x 中支持了 tree-shaking 所引起的变化
在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。
1 2 3 4 5 6 7 8 9 10
| import Vue from "vue"; Vue.nextTick(() => { });
import { nextTick } from "vue"; nextTick(() => { });
|
直接调用 Vue.nextTick()将会导致 undefined is not a function
通过这一更改,Vue 应用程序中未使用的全局 api 将从最终捆绑包中消除,从而获得最佳文件大小
受到影响的 Api
- Vue.nextTick
- vue.observable
- Vue.version
- Vue.compile
- Vue.set
- vue.delete
内部帮助器
除了公共 api,许多内部组件/帮助其现在也被导出为命名导出,只有当编译器的输出是这些特性时,才允许编译器导入这些特性
1 2 3
| <transition> <div v-show="ok">hello</div> </transition>
|
将会被编译为
1 2 3 4 5
| import { h, Transition, withDirectives, vShow } from 'vue
export function render() { return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])]) }
|
这意味着只有在应用程序实际使用了 Transition 组件的时候才会导入他
1
| 以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。
|
在插件中的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const plugin = { install: (Vue) => { Vue.nextTick(() => { }); }, };
import { nextTick } from "vue"; const plugin = { install: (app) => { nextTick(() => { }); }, };
|
如果使用 webpack 这样的模块捆绑包,这可能会导致 Vue 的源代码绑定到插件中,而且通常情况下,这并不是你所期望的。防止这种情况发生的一种常见做法是配置模块绑定器以将 Vue 从最终捆绑中排除。对于 webpack,你可以使用 externals 配置选项:
1 2 3 4 5 6 7
| module.exports = { externals: { vue: "Vue", }, };
|
如果你选择的模块绑定器恰好是 Rollup,你基本上可以免费获得相同的效果,因为默认情况下,Rollup 会将绝对模块 id (在我们的例子中为 ‘vue’) 作为外部依赖项,而不会将它们包含在最终的 bundle 中。但是在绑定期间,它可能会发出一个“将 vue 作为外部依赖” 警告,可使用 external 选项抑制该警告:
1 2 3 4 5
| export default { external: ["vue"], };
|
模板指令
按键修饰符
1
| 从KeyboardEvent.keyCode has been deprecated 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称。
|
1 2 3 4 5
| <input v-on:keyup.13="submit" /> <input v-on:keyup.enter="submit" />
<input v-on:keyup.delete="confirmDelete" />
|
同时废弃了全局 config.keyCodes 选项
key 属性
- v-if/v-else/v-else-if 的 key 不再是必须的,vue3.x 会自动生成唯一 key
不可以通过手动提供 key 的方式,来强制重用分支
1 2 3 4 5 6 7 8 9 10 11
| <div v-if="condition" key="yes">Yes</div> <div v-else key="no">No</div>
<div v-if="condition" key="a">Yes</div> <div v-else key="a">No</div>
<div v-if="condition" key="a">Yes</div> <div v-else key="b">No</div>
|
<template v-for>
的 key 应该设置在<template>
标签上,而不是设置在他的子结点上
1 2 3 4 5 6 7 8 9 10 11
| <template v-for="item in list"> <div :key="item.id">...</div> <span :key="item.id">...</span> </template>
<template v-for="item in list" :key="item.id"> <div>...</div> <span>...</span> </template>
|
v-if 和 v-for 的优先级调整
这个可太棒了
1 2 3 4 5
| 在vue3中,v-if拥有比v-for更高的优先级
官网建议: 由于语法上存在歧义,建议避免在同一元素上同时使用两者。 比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
|
v-bind 现在对排序敏感(v-bind 的合并行为)
如果在一个元素上同时定义了 v-bind=”object”和一个相同的单独的 property
那么 v-bind 的绑定会被覆盖
在 vue3.x 中 v-bind 和单独的 property 有排序关系,看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
<div id="red" v-bind="{ id: 'blue' }"></div>
<div id="red"></div>
<div id="red" v-bind="{ id: 'blue' }"></div>
<div id="blue"></div>
<div v-bind="{ id: 'blue' }" id="red"></div>
<div id="red"></div>
|
v-on 的 .native 修饰符已被移除
1 2
| vue3.x中新增了emits选项 对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <my-component v-on:close="handleComponentEvent" v-on:click.native="handleNativeClickEvent" />
<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" /> <script> export default { emits: ["close"], }; </script>
|
v-for 中的 ref 不再注册 ref 数组
在 Vue 2 中,在 v-for 里使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。
在 Vue 3 中,这样的用法将不再在 $ref 中自动创建数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <div v-for="item in list" :ref="setItemRef"></div>
<script> export default { data() { return { itemRefs: [], }; }, methods: { setItemRef(el) { this.itemRefs.push(el); }, }, beforeUpdate() { this.itemRefs = []; }, updated() { console.log(this.itemRefs); }, }; </script>
<script> 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, }; }, }; </script>
|
itemRefs 不必是数组:它也可以是一个对象,其 ref 会通过迭代的 key 被设置。
如果需要,itemRef 也可以是响应式的且可以被监听。
渲染函数
渲染函数 API 变更
此更改不会影响到<template>
用户
h
现在全局导入,而非作为参数传递给渲染函数
- 渲染函数参数更改为在有状态组件和函数组件之间更加一致
- vnode 现在又一个扁平的 prop 结构
Render 函数参数
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default { render(h) { return h('div') } }
export default { render() { return h('div') } }
|
渲染函数签名更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export default { render(h) { return h('div') } }
import { h, reactive } from 'vue' export default { setup(prop, {slots, attrs, emit}) { const state = reactive({ count: 0 }) function increment() { state.count++ } return () => h( 'div', { onClick: increment }, state.count ) } }
|
VNode Props 格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { class: ['button', 'is-outlined'], style: {color: '#fffff'}, attr: {id: 'submit'}, domProps: {innerHTML: ''}, on: {click: submitForm}, key: 'submit-button' }
{ class: ['button', 'is-outlined'], style: { color: '#34495E' }, id: 'submit', innerHTML: '', onClick: submitForm, key: 'submit-button' }
|
slot 统一
更改了普通 slot 和作用域 slot
this.$slots
现在将 slots 作为函数公开
- 移除
this.$scopedSlots
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| h(LayoutComponent, [ h("div", { slot: "header" }, this.header), h("div", { slot: "header" }, this.header), ]);
h( LayoutComponent, {}, { header: () => h("div", this.header), content: () => h("div", this.content), } );
this.$scopedSlots.header;
this.$slots.header;
|
移除$listeners
$listeners
对象在 vue3 中已经移除,现在事件监听器是$attrs
的一部分
在 vue2 中,可以使用 this.$attrs和this.$listeners 分别访问传递给组件的 attribute 和时间监听器,结合 inheritAttrs: false,开发者可以将这些 attribute 和监听器应用到其他元素,而不是根元素
1 2 3 4 5 6 7 8 9 10
| <template> <label> <input type="text" v-bind="$attrs" v-on="$listeners" /> </label> </template> <script> export default { inheritAttrs: false, }; </script>
|
在 vue 的虚拟 DOM 中,事件监听器现在只是以 on 为前缀的 attribute,这样就成了$attrs对象的一部分,这样$listeners 就被移除了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <label> <input type="text" v-bind="$attrs" /> </label> </template> <script> export default { inheritAttrs: false } { id: 'my-input', onClose: () => console.log('close Event Triggered') } </script>
|
$attrs 现在包括 class 和 style
现在的$attr 包含所有的 attribute,包括 class 和 style
在 2.x 中,虚拟 dom 会对 class 和 style 进行特殊处理,所以他们不包括在$attr 中
在使用 inheritAttr: false 的时候会产生副作用
- $attrs 中的 attribute 不再自动添加到根元素中,而是由开发者决定在哪添加。
- 但是 class 和 style 不属于 $attrs,仍然会应用到组件的根元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <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>
<label class="my-class"> <input type="text" id="my-id" /> </label>
<label> <input type="text" id="my-id" class="my-class" /> </label>
|
自定义元素
自主定制元素
如果我们先添加在 Vue 外部定义的自定义元素,如使用 Web 组件 API,我们需要指示 Vue 将其视为自定义元素:
1
| <plastic-button></plastic-button>
|
1 2 3 4
|
Vue.config.ignoredElements = ["plastic-button"];
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
rules: [ { test: /\.vue$/, use: "vue-loader", options: { compilerOptions: { isCustomElement: (tag) => tag === "plastic-button", }, }, }, ];
const app = Vue.createApp({}); app.config.isCustomElement = (tag) => tag === "plastic-button";
|
定义内置元素
自定义元素规范提供了一种将自定义元素用作自定义内置模板的方法,方法是向内置元素添加 is 属性:
1
| <button is="plastic-button">点击我!</button>
|
Vue 对 is 特殊 prop 的使用是在模拟 native attribute 在浏览器中普遍可用之前的作用,但是在 2.x 中,它被解释为一个名为 plastic-button 的 Vue 组件,浙江组织上面提到的自定义内置元素的原生使用
在 3.0 中,Vue 对 is 属性的特殊处理被限制到<component>
标签上
在保留的 <component>
tag 上使用时,它的行为将与 2.x 中完全相同
- 在普通组件上使用时,他的行为将类似于普通 prop
1 2 3
| <foo is="bar" /> 在vue2中,将会渲染bar组件 在vue3中,会通过is属性渲染foo组件
|
- 在普通元素上使用时,它将作为 is 选项传递给 createElement 调用,并作为原生属性渲染
1 2 3 4
| <button is="plastic-button">点击我!</button> 在vue2中,渲染plastic-button组件 在vue3中,渲染原生button: document.createElement('button', { is: 'plastic-button' })
|
v-is 用于 DOM 内模板解析解决方案
仅影响直接在页面的 HTML 中写入 Vue 模板的情况,在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <table> <tr is="blog-post-row"></tr> </table>
<table> <tr v-is="'blog-post-row'"></tr> </table>
<tr v-is="blog-post-row"></tr>
<tr v-is="'blog-post-row'"></tr>
|