TypeScript
支持不友好(所有属性都放在了this
对象上,难以推倒组件的数据类型)API
挂载在Vue
对象的原型上,难以实现TreeShaking。Vue 3 中需要关注的一些新特性包括:
)*@vue/runtime-core
的 createRenderer
API 用来创建自定义渲染函数
中的 v-bind
)*
新增全局规则和针对插槽内容的规则***** 现在也支持在 Vue 2.7 中使用
** Vue 2.7 中支持,但仅用于类型推断
Vue 3 的支持库进行了重大更新。以下是新的默认建议的摘要:
@vue/babel-preset-jsx
-> @vue/babel-plugin-jsx
详情说明
createApp
调用 createApp
返回一个应用实例,一个 Vue 3 中的新概念。
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
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 | 移除 |
Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
Vue.extend | 移除 |
所有其他不全局改变行为的全局 API 现在都是具名导出,文档见全局 API Treeshaking。
config.productionTip
移除在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建版本) 时才会显示。
对于 ES 模块构建版本,由于它们是与打包器一起使用的,而且在大多数情况下,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.extend
移除在 Vue 2.x 中,Vue.extend
曾经被用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。在 Vue 3.x 中,我们已经没有组件构造器的概念了。应该始终使用 createApp
这个全局 API 来挂载组件:
// 之前 - Vue 2
// 创建构造器
const Profile = Vue.extend({
template: '{{firstName}} {{lastName}} aka {{alias}}
',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建一个 Profile 的实例,并将它挂载到一个元素上
new Profile().$mount('#mount-point')
// 之后 - Vue 3
const Profile = {
template: '{{firstName}} {{lastName}} aka {{alias}}
',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
}
Vue.createApp(Profile).mount('#mount-point')
在 UMD 构建中,插件开发者使用 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)
使用 createApp(/* options */)
初始化后,应用实例 app
可通过 app.mount(domTarget)
挂载根组件实例:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
经过所有的这些更改,我们在指南开头编写的组件和指令现在将被改写为如下内容:
const app = createApp(MyApp)
// 注册组件
app.component('button-counter', {
data: () => ({
count: 0
}),
template: ''
})
// 注册指令
app.directive('focus', {
mounted: (el) => el.focus()
})
// 现在,所有通过 app.mount() 挂载的应用实例及其组件树,
// 将具有相同的 “button-counter” 组件和 “focus” 指令,
// 而不会污染全局环境
app.mount('#app')
与在 2.x 根实例中使用 provide
选项类似,Vue 3 应用实例也提供了可被应用内任意组件注入的依赖项:
// 在入口中
app.provide('guide', 'Vue 3 Guide')
// 在子组件中
export default {
inject: {
book: {
from: 'guide'
}
},
template: `{{ book }}`
}
在编写插件时使用 provide
将尤其有用,可以替代 globalProperties
。
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
const app = Vue.createApp({
/* 选项 */
})
传递给 createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果你想把一个 Vue 应用挂载到 ,应该传入
#app
:
const app = Vue.createApp({
/* 选项 */
})
// 组件配置对象,删除el属性,使用 .mount('#app') 挂载组件实例
const vm = app.mount('#app')
与大多数应用方法不同的是,mount
不返回应用本身。相反,它返回的是根组件实例。
组件的 data
选项必须是一个函数。Vue 会在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也会直接通过组件实例暴露出来:
var app = Vue.createApp({
// 组件的data必须是一个函数
data(){
return {
count: 4,
}
},
});
const vm = app.mount('#app')
console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4
value
-> modelValue
;input
-> update:modelValue
;v-bind
的 .sync
修饰符和组件的 model
选项已移除,可在 v-model
上加一个参数代替;v-model
绑定;v-model
修饰符。用于自定义组件时,v-model
prop 和事件默认名称已更改:
value
-> modelValue
;input
-> update:modelValue
;在 3.x 中,自定义组件上的 v-model
相当于传递了 modelValue
prop 并接收抛出的 update:modelValue
事件:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
>custom-input>
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
`;
})
现在 v-model 就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText">custom-input>
在自定义组件中创建 v-model 功能的另一种方法是使用 computed 的功能来定义 getter 和 setter
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: ``,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})
Vue 3 现在提供一个 emits
选项,和现有的 props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
app.component('custom-form', {
emits: ['inFocus', 'submit']
})
当在 emits
选项中定义了原生事件 (如 click
) 时,将使用组件中的事件替代原生事件侦听器。
建议定义所有发出的事件,以便更好地记录组件应该如何工作。
与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以对它进行验证。
要添加验证,请为事件分配一个函数,该函数接收传递给 $emit
调用的参数,并返回一个布尔值以指示事件是否有效。
app.component('custom-form', {
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
v-model
参数默认情况下,组件上的 v-model
使用 modelValue
作为 prop 和 update:modelValue
作为事件。若需要更改 model
的名称,现在我们可以为 v-model
传递参数来修改这些名称,以作为组件内 model
选项的替代:
<my-component v-model:title="pageTitle">my-component>
<my-component :title="pageTitle" @update:title="pageTitle = $event">my-component>
在本例中,子组件将需要一个 title
prop 并发出 update:title
事件来进行同步:
app.component('my-component', {
props: ['title'],
emits: ['update:title'],
template: `
`
})
这也可以作为 .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
绑定正如我们之前在 v-model
参数中所学的那样,通过利用以特定 prop 和事件为目标的能力,我们现在可以在单个组件实例上创建多个 v-model 绑定。
v-model:modelValue
简写为 v-model
,省略了 modelValue
v-model:firstName
每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:
<user-name
v-model="age"
v-model:firstName="firstName"
v-model:lastName="lastName"
>user-name>
app.component('user-name', {
props: {
modelValue: Number,
firstName: String,
lastName: String
},
emits: ['update:modelValue', 'update:firstName', 'update:lastName'],
template: `
`
})
v-model
修饰符除了像 .trim
这样的 2.x 硬编码的 v-model
修饰符外,现在 3.x 还支持自定义修饰符:
<my-component v-model.capitalize.x="text">my-component>
app.component('my-component', {
props: {
modelValue: String,
// 添加到 v-model 的修饰符会自动添加到 modelModifiers 对象上 提供给组件
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: ``,
methods: {
emitValue(e) {
// console.log(this.modelModifiers); // {capitalize: true, x: true}
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + "Modifiers"
:
<my-div v-model:text.capitalize.x="text">my-div>{{text}}
app.component('my-component', {
props: {
modelValue: String,
// 添加到 v-model 的修饰符将自定添加到 textModifiers 对象上提供给组件
textModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: ``,
methods: {
emitValue(e) {
// console.log(this.textModifiers); // {capitalize: true, x: true}
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
key
v-if
/v-else
/v-else-if
的各分支项 key
将不再是必须的,因为现在 Vue 会自动生成唯一的 key
。
key
,那么每个分支必须使用唯一的 key
。你将不再能通过故意使用相同的 key
来强制重用分支。
的 key
应该设置在
标签上 (而不是设置在它的子节点上)。特殊的 key
attribute 被作为 Vue 的虚拟 DOM 算法的提示,以保持对节点身份的持续跟踪。这样 Vue 就可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。关于其它更多信息,可以查看以下章节:
在 Vue 2.x 中,建议在 v-if
/v-else
/v-else-if
的分支中使用 key
。
<div v-if="condition" key="yes">Yesdiv>
<div v-else key="no">Nodiv>
这个示例在 Vue 3.x 中仍能正常工作。但是我们不再建议在 v-if
/v-else
/v-else-if
的分支中继续使用 key
attribute,因为没有为条件分支提供 key
时,也会自动生成唯一的 key
。
<div v-if="condition">Yesdiv>
<div v-else>Nodiv>
非兼容变更体现在如果你手动提供了 key
,那么每个分支都必须使用一个唯一的 key
。因此大多数情况下都不需要设置这些 key
。
<div v-if="condition" key="a">Yesdiv>
<div v-else key="a">Nodiv>
<div v-if="condition">Yesdiv>
<div v-else>Nodiv>
<div v-if="condition" key="a">Yesdiv>
<div v-else key="b">Nodiv>
在 Vue 2.x 中, 标签不能拥有
key
。不过,你可以为其每个子节点分别设置 key
。
<template v-for="item in list">
<div :key="'heading-' + item.id">...div>
<span :key="'content-' + item.id">...span>
template>
在 Vue 3.x 中,key
则应该被设置在 标签上。
<template v-for="item in list" :key="item.id">
<div>...div>
<span>...span>
template>
类似地,当使用 时如果存在使用
v-if
的子节点,则 key
应改为设置在 标签上。
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...div>
<span v-else :key="item.id">...span>
template>
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...div>
<span v-else>...span>
template>
v-if
会拥有比 v-for
更高的优先级。Vue.js 中使用最多的两个指令就是 v-if
和 v-for
,因此开发者们可能会想要同时使用它们。虽然不建议这样做,但有时确实是必须的,于是我们想提供有关其工作方式的指南。
2.x 版本中在一个元素上同时使用 v-if
和 v-for
时,v-for
会优先作用。
3.x 版本中 v-if
总是优先于 v-for
生效。
在一个元素上动态绑定 attribute 时,同时使用 v-bind="object"
语法和独立 attribute 是常见的场景。然而,这就引出了关于合并的优先级的问题。
在 2.x 中,如果一个元素同时定义了 v-bind="object"
和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object
中的绑定。
<div id="red" v-bind="{ id: 'blue' }">div>
<div id="red">div>
在 3.x 中,如果一个元素同时定义了 v-bind="object"
和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。换句话说,相对于假设开发者总是希望独立 attribute 覆盖 object
中定义的内容,现在开发者能够对自己所希望的合并行为做更好的控制。
<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
修饰符v-on
的 .native
修饰符已被移除。
默认情况下,传递给带有 v-on
的组件的事件监听器只能通过 this.$emit
触发。要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native
修饰符:
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
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>
const app = Vue.createApp({
// 注册局部组件
components: {
'my-div3':{},
'my-div4':{},
}
});
// 注册全局组件
app.component('my-div1', {
//组件的模板中可以添加多个根标签
});
app.component('my-div2', {
//组件的模板中可以添加多个根标签
});
app.mount('#app);
emits
选项 新增Vue 3 现在提供一个 emits
选项,和现有的 props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件:
{{ text }}
3.x 的行为
和 prop 类似,现在可以通过
emits
选项来定义组件可触发的事件:
{{ text }}
该选项也可以接收一个对象,该对象允许开发者定义传入事件参数的验证器,和
props
定义里的验证器类似。迁移策略
强烈建议使用
emits
记录每个组件所触发的所有事件。这尤为重要,因为我们移除了
.native
修饰符。任何未在emits
中声明的事件监听器都会被算入组件的$attrs
,并将默认绑定到组件的根节点上。示例
对于向其父组件透传原生事件的组件来说,这会导致有两个事件被触发:
当一个父级组件拥有
click
事件的监听器时:<my-button v-on:click="handleClick">my-button>
该事件现在会被触发两次:
$emit()
。现在你有两个选项:
click
事件。当你真的在
的事件处理器上加入了一些逻辑时,这会很有用。.native
。适用于你只想透传这个事件。以下是对变化的总体概述:
defineAsyncComponent
助手方法,用于显式地定义异步组件component
选项被重命名为 loader
resolve
和 reject
参数,且必须返回一个 Promise以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncModal = () => import('./Modal.vue')
或者,对于带有选项的更高阶的组件语法:
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
现在,在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent
助手方法中来显式地定义:
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
注意
Vue Router 支持一个类似的机制来异步加载路由组件,也就是俗称的懒加载。尽管类似,但是这个功能和 Vue 所支持的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该使用 defineAsyncComponent
。你可以在 Vue Router 文档的懒加载路由章节阅读更多相关内容。
对 2.x 所做的另一个更改是,component
选项现在被重命名为 loader
,以明确组件定义不能直接被提供。
import { defineAsyncComponent } from 'vue'
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
此外,与 2.x 不同,loader 函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise。
// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
/* ... */
}
// 3.x 版本
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
Vue 3 现在正式支持了多根节点的组件,也就是片段!
如果你的组件有多个根元素,你需要定义哪些部分将接收这个类。可以使用 $attrs 组件属性执行此操作
<my-div :class="classObj" class="static">my-div>
template:`
<div :class="$attrs.class">1div>
<div>2div>
`,
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
Teleport
是一种能够将我们的组件html结构移动到指定位置的技术。<style>
.modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
background-color: white;
width: 300px;
height: 200px;
padding: 5px;
}
style>
<div id="app">
<div style="position: relative;" class="outer">
<h3>传送门h3>
<div class="box">
<modal-button>modal-button>
div>
div>
div>
<script id="modal-button" type="text/html">
<button @click="modalOpen = true">打开全屏的弹窗</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
全屏的模态框,父元素是body标签
<button @click="modalOpen = false">关闭</button>
</div>
</div>
</teleport>
script>
<script>
const app = Vue.createApp({});
app.component('modal-button', {
template: `#modal-button`,
data () {
return {
modalOpen: false
}
}
})
app.mount('#app');
script>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
使用Suspense
包裹组件,并配置好default
与 fallback
我是App组件
加载中.....
在初始渲染时,
将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,
会进入完成状态,并将展示出默认插槽的内容。
如果在初次渲染时没有遇到异步依赖,
会直接进入完成状态。
进入完成状态后,只有当默认插槽的根节点被替换时,
才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成
回退到挂起状态。
发生回退时,后备内容不会立即展示出来。相反,
在等待新内容和异步依赖完成时,会展示之前 #default
插槽的内容。这个行为可以通过一个 timeout
prop 进行配置:在等待渲染新内容耗时超过 timeout
之后,
将会切换为展示后备内容。若 timeout
值为 0
将导致在替换默认内容时立即显示后备内容。
组件会触发三个事件:pending
、resolve
和 fallback
。pending
事件是在进入挂起状态时触发。resolve
事件是在 default
插槽完成获取新内容时触发。fallback
事件则是在 fallback
插槽的内容显示时触发。
例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器。
此更改不会影响 以下是更改的简要总结: 请继续阅读来获取更多信息! 在 2.x 中, 在 3.x 中, 在 2.x 中, 在 3.x 中,整个 VNode prop 的结构都是扁平的。使用上面的例子,来看看它现在的样子。 在 2.x 中,注册一个组件后,把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作: 在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 此更改统一了 3.x 中的普通插槽和作用域插槽。 以下是变化的变更总结: 当使用渲染函数,即 此外,可以使用以下语法引用作用域插槽: 在 3.x 中,插槽以对象的形式定义为当前节点的子节点: 当你需要以编程方式引用作用域插槽时,它们现在被统一到 在 Vue 2 中,你可以通过 在 Vue 3 的虚拟 DOM 中,事件监听器现在只是以 如果这个组件接收一个 Vue 2 的虚拟 DOM 实现对 上述行为在使用 像这样使用时: ……将生成以下 HTML: Vue3中 与单个根节点组件不同,具有多个根节点的组件不具有自动 属性 贯穿行为。如果未显式绑定 $attrs,将发出运行时警告。 以下是变更的简要总结: 在 Vue 2 中, 此外,也可以通过全局的 从 因此,这意味着 在 2.x 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ( 我们从实例中完全移除了 从 Vue 3.0 开始,过滤器已移除,且不再支持。 在 2.x 中,开发者可以使用过滤器来处理通用文本格式。 例如: 虽然这看起来很方便,但它需要一个自定义语法,打破了大括号内的表达式“只是 JavaScript”的假设,这不仅有学习成本,而且有实现成本。 在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。 以上面的案例为例,以下是一种实现方式。 在 2.x 中,开发者可以使用 在 3.x 中, 指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。 额外地, 在 Vue 2 中,自定义指令通过使用下列钩子来创建,以对齐元素的生命周期,它们都是可选的: 下面是一个例子: 此处,在这个元素的初始设置中,通过给指令传递一个值来绑定样式,该值可以在应用中任意更改。 然而,在 Vue 3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使钩子的目标事件十分相似。我们现在把它们统一起来了: 最终的 API 如下: 因此,API 可以这样使用,与前面的示例相同: 既然现在自定义指令的生命周期钩子与组件本身保持一致,那么它们就更容易被推理和记住了! 通常来说,建议在组件实例中保持所使用的指令的独立性。从自定义指令中访问组件实例,通常意味着该指令本身应该是一个组件。然而,在某些情况下这种用法是有意义的。 在 Vue 2 中,必须通过 在 Vue 3 中,实例现在是 WARNING 有了片段的支持,组件可能会有多个根节点。当被应用于多根组件时,自定义指令将被忽略,并将抛出警告。 在 2.x 中,开发者可以通过 例如: 虽然这种做法对于具有共享状态的根实例提供了一些便利,但是由于其只可能存在于根实例上,因此变得混乱。 在 3.x 中, 使用上面的示例,代码只可能有一种实现: 此外,当来自组件的 在 Vue 2.x 中,生成的 在 3.0 中,其结果将会是: 在 Vue 2.x 中,当挂载一个具有 在 Vue 2.x 中,我们为 当我们把应用挂载到拥有匹配被传入选择器 (在这个例子中是 在渲染结果中,上面提及的 在 Vue 3.x 中,当我们挂载一个应用时,其渲染内容会替换我们传递给 当这个应用挂载到拥有匹配 生成 prop 默认值的工厂函数不再能访问 取而代之的是: 当使用 如果你依赖于侦听数组的改变,添加 通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。 对于这种情况,我们可以使用一对 实际上,你可以将依赖注入看作是“长距离的 prop”,除了: 在 Vue 2 中,在 在 Vue 3 中,此类用法将不再自动创建 过渡样式类名的变化: 3个进入过渡类: v-enter-from:定义进入过渡的开始状态。 v-enter-to:定义进入过渡的结束状态。 v-enter-active:定义进入过渡生效时的状态。 3个离开过渡类: v-leave-from:定义离开过渡的开始状态。 v-leave-to:定义离开过渡生效时的状态。 v-leave-active:离开过渡的结束状态。 在 v2.1.8 版本之前,每个过渡方向都有两个过渡类:初始状态与激活状态。 在 v2.1.8 版本中,引入了 这样做会带来很多困惑,类似 enter 和 leave 含义过于宽泛,并且没有遵循类名钩子的命名约定。 为了更加明确易读,我们现在将这些初始状态重命名为: 现在,这些状态之间的区别就清晰多了。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwZnWzqm-1680606240145)(null)] 当使用 在 Vue 2 中,通过使用 切换 这是无意为之的,并不是设计效果。一个 这个怪异的现象现在被移除了。 换做向组件传递一个 prop 就可以达到类似的效果: 在 Vue 2 中, 在 Vue 3 中,我们有了片段的支持,因此组件不再需要根节点。所以, 如果你刚开始使用Vue3,很可能会发现,原本用得得心应手的eventBus(事件总线)突然不灵了。 因为Vue3不再提供 想在Vue3上把EventBus再次用起来也非常简单,大体就是三个步骤 不需要在入口文件中编写额外逻辑,不需要每次引入inject函数,不需要每次为bus赋值,import进来一把梭直接用。 更多用法和配置可以参照github上的文档 这两种方式都没啥差别,因为代码逻辑也很简单,贴一个代码实现,可以直接copy去用 入口文件默认是main.js 在created中使用 在setup中使用 注意: 因为在setup中无法访问到应用实例( 通过上面三个步骤,EventBus就可以正常使用啦,还是很简单的。不过也能看到在挂载的时候需要多写两行代码,使用的时候,每个组件在setup内都要引入inject函数,并且初始化一次。有朋友就要问了?有没有更优雅的办法咧? 没错!用了 Vue3-Eventbus,只需要在入口文件里use一下,每个组件里引入就能直接用起来啦! Vue的响应式是怎么实现的? Vue3的响应式 优于 Vue2响应式,两个版本响应式原理的不同也是体现在 Vue2.0 有哪些缺点或者说不足: 如果你只说 Vue2.0 是基于Object.definePropery;Vue3.0是基于ES6的proxy来架构的,仅此而已的话,那显然是不够的。 大家都知道Vue2的响应式是基于 上面使用 data新增了 从上面,知道了 可以看到,其实效果与上面的 Vue2.0 Vue3.0 用户。
h
现在是全局导入,而不是作为参数传递给渲染函数渲染函数参数
2.x 语法
render
函数会自动接收 h
函数 (它是 createElement
的惯用别名) 作为参数:// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}
3.x 语法
h
函数现在是全局导入的,而不是作为参数自动传递。// Vue 3 渲染函数示例
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
VNode Prop 格式化
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 语法
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '',
onClick: submitForm,
key: 'submit-button'
}
注册组件
2.x 语法
// 2.x
Vue.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
`
})
export default {
render(h) {
return h('button-counter')
}
}
3.x 语法
resolveComponent
方法:// 3.x
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter')
return () => h(ButtonCounter)
}
}
插槽统一 非兼容
概览
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 Syntax
h(LayoutComponent, {}, {
header: () => h('div', this.header),
content: () => h('div', this.content)
})
$slots
选项中了。// 2.x 语法
this.$scopedSlots.header
// 3.x 语法
this.$slots.header()
移除
$listeners
概览
$listeners
对象在 Vue 3 中已被移除。事件监听器现在是 $attrs
的一部分:{
text: '这是一个 attribute',
onClose: () => console.log('close 事件被触发')
}
2.x 语法
this.$attrs
访问传递给组件的 attribute,以及通过 this.$listeners
访问传递给组件的事件监听器。结合 inheritAttrs: false
,开发者可以将这些 attribute 和监听器应用到根元素之外的其它元素:<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
label>
template>
<script>
export default {
inheritAttrs: false
}
script>
3.x 语法
on
为前缀的 attribute,这样它就成为了 $attrs
对象的一部分,因此 $listeners
被移除了。
id
attribute 和一个 v-on:close
监听器,那么 $attrs
对象现在将如下所示:{
id: 'my-input',
onClose: () => console.log('close 事件被触发')
}
$attrs
包含 class
& style
概览
$attrs
现在包含了所有传递给组件的 attribute,包括 class
和 style
。2.x 行为
class
和 style
attribute 有一些特殊处理。因此,与其它所有 attribute 不一样,它们没有被包含在 $attrs
中。inheritAttrs: false
时会产生副作用:
$attrs
中的 attribute 将不再被自动添加到根元素中,而是由开发者决定在哪添加。class
和 style
不属于 $attrs
,它们仍然会被应用到组件的根元素中:
<my-component id="my-id" class="my-class">my-component>
<label class="my-class">
<input type="text" id="my-id" />
label>
3.x 行为
$attrs
现在包含了所有传递给组件的 attribute,包括 class
和 style
,这使得把它们全部应用到另一个元素上变得更加容易了。现在上面的示例将生成以下 HTML:<label>
<input type="text" id="my-id" class="my-class" />
label>
<script id="my-div" type="text/html">
<div class="my-div" v-bind="$attrs"></div>
<div></div>
script>
移除的 APIs
按键修饰符
概览
v-on
修饰符config.keyCodes
2.x 语法
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
已被废弃开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 名称。
<input v-on:keyup.page-down="nextPage">
<input v-on:keypress.q="quit">
config.keyCodes
现在也已弃用,不再受支持。事件 API
概览
$on
,$off
和 $once
实例方法已被移除,组件实例不再实现事件触发接口。2.x 语法
$on
,$off
和 $once
)。这可以用于创建一个事件总线,以创建在整个应用中可用的全局事件监听器:// eventBus.js
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {
mounted() {
// 添加 eventBus 监听器
eventBus.$on('custom-event', () => {
console.log('Custom event triggered!')
})
},
beforeDestroy() {
// 移除 eventBus 监听器
eventBus.$off('custom-event')
}
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
methods: {
callGlobalCustomEvent() {
eventBus.$emit('custom-event') // 当 ChildComponent 已被挂载时,控制台中将显示一条消息
}
}
}
3.x 更新
$on
、$off
和 $once
方法。$emit
仍然包含于现有的 API 中,因为它用于触发由父组件声明式添加的事件处理函数。过滤器
概览
2.x 语法
<template>
<h1>Bank Account Balanceh1>
<p>{{ accountBalance | currencyUSD }}p>
template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
script>
3.x 更新
<template>
<h1>Bank Account Balanceh1>
<p>{{ accountInUSD }}p>
template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
script>
$children
概览
$children
实例 property 已从 Vue 3.0 中移除,不再支持。2.x 语法
this.$children
访问当前实例的直接子组件:
3.x 更新
$children
property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用模板引用。其他变化
自定义指令
概览
expression
字符串不再作为 binding
对象的一部分被传入。2.x 语法
<p v-highlight="'yellow'">以亮黄色高亮显示此文本p>
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
3.x 语法
updated
有太多相似之处,因此它是多余的。请改用 updated
。const MyDirective = {
created(el, binding, vnode, prevVnode) {}, // 新增
beforeMount() {},
mounted() {},
beforeUpdate() {}, // 新增
updated() {},
beforeUnmount() {}, // 新增
unmounted() {}
}
<p v-highlight="'yellow'">以亮黄色高亮显示此文本p>
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
边界情况:访问组件实例
vnode
参数访问组件实例:bind(el, binding, vnode) {
const vm = vnode.context
}
binding
参数的一部分:mounted(el, binding, vnode) {
const vm = binding.instance
}
Data 选项
概览
data
的声明不再接收纯 JavaScript object
,而是接收一个 function
。data
返回值时,合并操作现在是浅层次的而非深层次的 (只合并根级属性)。2.x 语法
object
或者是 function
定义 data
选项。
<script>
const app = new Vue({
data: {
apiKey: 'a1b2c3'
}
})
script>
<script>
const app = new Vue({
data() {
return {
apiKey: 'a1b2c3'
}
}
})
script>
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
}
}
}
}
$data
是:{
"user": {
"id": 2,
"name": "Jack"
}
}
{
"user": {
"id": 2
}
}
被挂载的应用不会替换元素
概述
template
的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML
。2.x 语法
new Vue()
或 $mount
传入一个 HTML 元素选择器:new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue!'
}
},
template: `
id="app"
) 的 div
的页面时:<body>
<div id="app">
Some app content
div>
body>
div
将会被应用所渲染的内容替换:<body>
<div id="rendered">Hello Vue!div>
body>
3.x 语法
mount
的元素的 innerHTML
:const app = Vue.createApp({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
id="app"
的 div
的页面时,结果会是:<body>
<div id="app" data-v-app="">
<div id="rendered">Hello Vue!div>
div>
body>
prop 的默认函数中访问
this
this
。
import { inject } from 'vue'
export default {
props: {
theme: {
default (props) {
// `props` 是传递给组件的、
// 在任何类型/默认强制转换之前的原始值,
// 也可以使用 `inject` 来访问注入的 property
return inject('theme', 'default-theme')
}
}
}
}
侦听数组
概览
deep
选项。3.x 语法
watch
选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定 deep
选项。watch: {
bookList: {
handler(val, oldVal) {
console.log('book list changed')
},
deep: true
},
}
迁移策略
deep
选项以确保回调能被正确地触发。提供注入 Provide / Inject
provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
<div id="app">
<input type="number" v-model.number="number">
<my-div>my-div>
div>
const app = Vue.createApp({
data () {
return {
message: 'hello app!',
number: 10,
}
},
// 要访问组件实例的属性,我们需要将 provide 定义为函数,并在函数中返回对象
provide () {
return {
user: '张三',
msg: this.message,
// 想对祖先组件中的更改做出反应,我们需要为我们提供的 num 分配一个组合式 API computed 属性
num: Vue.computed(() => this.number),
}
}
});
app.component('my-div', {
name: 'my-div',
inject: ['user'],
template: `
生命周期函数的变化
v-for 中的 Ref 数组
v-for
中使用的 ref
属性会用 ref 数组填充相应的 $refs
。当存在嵌套的 v-for
时,这种行为会变得不明确且效率低下。$ref
数组。要从单个绑定获取多个 ref,请将 ref
绑定到一个更灵活的函数上 (这是一个新特性):<div v-for="item in list" :ref="setItems">div>
export default {
data() {
return {
list:[2,4,6,8],
divs: []
}
},
methods: {
setItems(el) {
if (el) {
this.divs.push(el)
}
},
},
// 确保在每次更新之前重置divs
beforeUpdate() {
this.divs = []
},
}
动画的变化
过渡的 class 名更改
概览
v-enter
修改为 v-enter-from
v-leave
修改为 v-leave-from
组件的相关 prop 名称也发生了变化:
leave-class
已经被重命名为 leave-from-class
enter-class
已经被重命名为 enter-from-class
2.x 语法
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;
}
组件的相关 prop 名称也发生了变化:
leave-class
已经被重命名为 leave-from-class
(在渲染函数或 JSX 中可以写为:leaveFromClass
)enter-class
已经被重命名为 enter-from-class
(在渲染函数或 JSX 中可以写为:enterFromClass
)Transition 作为根节点
概览
作为根结点的组件从外部被切换时将不再触发过渡效果。2.x 行为
作为一个组件的根节点,过渡效果存在从组件外部触发的可能性:
<template>
<transition>
<div class="modal"><slot/>div>
transition>
template>
<modal v-if="showModal">hellomodal>
showModal
的值将会在模态组件内部触发一个过渡效果。
原本是希望被其子元素触发的,而不是
自身。迁移策略
<modal :show="showModal">hellomodal>
Transition Group 根元素
概览
不再默认渲染根元素,但仍然可以用 tag
attribute 创建根元素。2.x 语法
像其它自定义组件一样,需要一个根元素。默认的根元素是一个 ,但可以通过
tag
attribute 定制。<transition-group tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
li>
transition-group>
3.x 语法
不再默认渲染根节点。
tag
attribute,那么一切都会和之前一样tag
attribute,而且样式或其它行为依赖于 根元素的存在才能正常工作,那么只需将
tag="span"
添加到
:<transition-group tag="span">
transition-group>
Vue 3 如何使用eventBus
$on
函数,Vue实例不再实现事件接口。官方推荐引入外部工具实现,或者自己手撸一个事件类api变更文档
$on
,$off
和 $once
实例方法已被移除,应用实例不再实现事件触发接口。$emit
仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序如何使用
通过Vue3-Eventbus(推荐)
安装
$ npm install --save vue3-eventbus
挂载
import bus from 'vue3-eventbus'
app.use(bus)
使用
// Button.vue
import bus from 'vue3-eventbus'
export default {
setup() {
bus.emit('foo', { a: 'b' })
}
}
不借助插件的原生使用方式
引入/编写事件库
// eventBus.js
export default class EventBus{
constructor(){
this.events = {};
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(function(fn) {
fn(data);
});
}
}
on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
off(eventName, fn) {
if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1);
break;
}
};
}
}
}
在入口文件中执行挂载
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// ① 引入事件类
// 自己编写的或者mitt皆可
import EventBus from 'lib/bus.js'
// 或者:import EventBus from 'mitt'
const $bus = new EventBus()
// ② 挂载
// 1.使用provide提供
app.provide('$bus', $bus)
// 2.挂载到this上
app.config.globalProperties.$bus = $bus
在组件中引入并使用
// Button.vue
export default {
created() {
this.$bus.emit('ButtonCreated')
}
}
复制代码
this
),如果你需要在setup中使用eventBus,则需要通过provide/inject方式引入// Button.vue
import { inject } from 'vue'
export default {
setup() {
const $bus = inject('$bus')
$bus.emit('ButtonSetup')
}
}
使用小结
Vue3响应式原理
Vue3与Vue2的响应式原理
Object.defineProperty
实现的。Proxy
来实现的。Object.defineProperty
和Proxy
的差异上,那么Vue3的响应式到底比Vue2的响应式好在哪?
模拟Vue的响应式原理
Vue2的响应式
Object.defineProperty
的,那我就拿Object.defineProperty
来举个例子<script>
// 模拟 Vue 中的 data
let data = {
name: '张三',
age: 12
}
// 更新视图
function updateView (key, value) {
document.getElementById(key).innerHTML = key + '=' + value
}
// 模拟Vue的响应式函数reactive
function reactive (target, key, value) {
Object.defineProperty(target, key, {
get () {
console.log(`访问了${key}属性`)
return value
},
set (newValue) {
console.log(`将${key}由->${value}->设置成->${newValue}`)
if (value !== newValue) {
value = newValue;
updateView(key, newValue);
}
}
})
}
// 模拟 Vue 实例,代理data中的每一个属性,后增加的属性,不具有响应式
Object.keys(data).forEach(key => reactive(data, key, data[key]));
updateView('name', data.name);
updateView('age', data.age);
// 模拟值的改变
function addClick (key, value) {
if (data[key]) {
data[key] = data[key] + value;
} else {
data[key] = value;
}
}
script>
Object.defineProperty
定义的响应式函数到底有什么弊端呢?使得尤大大在Vue3中抛弃了它,如下例子:<div id="app">
<button onclick="addClick('age', 1)">点击改变agebutton>
<button onclick="addClick('weight', 2)">点击增加weight属性button>
<p id="name">p>
<p id="age">p>
<p id="weight">p>
div>
weight
属性,进行访问和设值,但是都不会触发get和set
,所以弊端就是:Object.defineProperty
只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么Vue2中对象新增属性的修改需要使用Vue.$set
来设值的原因。Vue3的响应式
Object.defineProperty
的弊端,Vue3中响应式原理的核心Proxy
是怎么弥补这一缺陷的,如下例子:<div id="app">
<button onclick="addClick('age', 1)">点击改变agebutton>
<button onclick="addClick('weight', 2)">点击增加weight属性button>
<p id="name">name=张三p>
<p id="age">age=12p>
<p id="weight">p>
div>
<script>
// 模拟 Vue 中的 data
let data = {
name: '张三',
age: 12
}
function updateView (key, value) {
document.getElementById(key).innerHTML = key + '=' + value
}
// 模拟vue的响应式函数
function reactive (target) {
const handler = {
get (target, key, receiver) {
console.log(`访问了${key}属性`)
return Reflect.get(target, key, receiver);
},
set (target, key, value, receiver) {
console.log(`将${key}由->${target[key]}->设置成->${value}`);
Reflect.set(target, key, value, receiver);
updateView(key, target[key]);
}
}
return new Proxy(target, handler)
}
// 模拟 Vue 实例,代理data中的每一个属性
// Proxy直接会代理监听data的内容,不管data有多少属性,使用proxy代理整个对象,非常的简单方便,而 defineProperty 需要手动循环代理每个对象里的每个属性
const vm = reactive(data);
updateView('name', data.name);
updateView('age', data.age);
// 模拟值的改变
function addClick (key, value) {
if (vm[key]) {
vm[key] = vm[key] + value;
} else {
vm[key] = value;
}
}
script>
Object.defineProperty
没什么差别,那为什么尤大大要抛弃它,选择Proxy
呢?注意了,最最最关键的来了,那就是对象新增属性,依然会触发get和set
,vm对象代理了data中的每一个属性。
Vue2.0和Vue3.0响应式的差异
Object.defineProperty
,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。Object.defineProperty
无法检测到对象属性的添加和删除 。
你可能感兴趣的:(vue,javascript,vue.js,前端)