Vue
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。
Vue 是一个典型的 MVVM 模型的框架。MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定。这使得 ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。
优点:
- 易于学习和使用:Vue提供了一个平滑的学习曲线,使其适用于初学者和专业开发人员。它有着丰富的文档和教程,并且有着庞大的社区支持。
- 高性能:Vue使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,Vue会根据新的状态创建一个新的虚拟DOM树。然后,Vue会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。
- 灵活性:Vue非常灵活,可以与现有项目无缝集成。它提供了许多高级功能,如计算属性、侦听器和过渡效果等,可以帮助开发人员更快地构建复杂的应用程序。
缺点:
- 生态系统不够成熟:相比其他前端框架,Vue的生态系统不够成熟。它缺少一些高质量的插件和工具,这可能会影响开发人员的工作效率。
- 文档不足:尽管Vue有着丰富的文档和教程,但由于其快速发展,有时文档可能不够完整或过时。
- 学习曲线陡峭:尽管Vue相对容易学习,但要真正掌握它并开发复杂的应用程序仍然需要一定的时间和精力。
既然提到了 mvvm,那么就简单说一下 MVC 以及 MVVM 和 MVC 之间的区别:
MVC 和 MVVM 都是一种设计模式,它们都旨在将应用程序分成不同的部分,以便更好地管理和维护。
MVC
MVC 是 Model-View-Controller 的缩写,它将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,Controller 负责接收用户输入并更新 Model 和 View。在 MVC 模式中,View 和 Model 是相互独立的,它们之间通过 Controller 来进行通信。
优点:
- 耦合度低:MVC 的三个部件(Model、View 和 Controller)是相互独立的,改变其中一个不会影响其他两个。
- 重用性高:多个视图可以使用同一个模型。
- 可维护性高:由于各个部件之间的分离,MVC 模式下的应用程序更容易维护。
缺点:
- 不适合小型项目开发。
- 视图与控制器联系过于紧密,妨碍了它们的独立重用。
MVVM
MVVM 是 Model-View-ViewModel 的缩写,它也将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,ViewModel 则负责连接 View 和 Model。与 MVC 不同的是,在 MVVM 模式中,View 和 ViewModel 之间有着双向数据绑定的联系。这意味着当 ViewModel 中的数据发生变化时,View 会自动更新;而当 View 中的数据发生变化时,ViewModel 也会自动更新。
优点:
- 低耦合:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上。当 View 变化时,Model 可以不变化;当 Model 变化时,View 也可以不变。
- 可重用性:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。
- 独立开发:双向数据绑定的模式实现了 View 和 Model 的自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要一直操作 DOM。
缺点:
- 增加了代码复杂度,并且对于简单的应用来说可能会显得过于繁琐。
- 由于 MVVM 模式依赖于双向数据绑定,因此它也可能会带来一些性能问题。
总之,MVC 和 MVVM 的主要区别在于它们对 View 和 Model 之间通信方式的不同处理。MVC 通过 Controller 来进行通信,而 MVVM 则通过双向数据绑定来实现通信。这两种模式各有优缺点,具体使用哪种模式取决于具体的应用场景。
底层实现原理
Vue 的底层实现原理主要包括数据双向绑定和虚拟 DOM两部分。
数据双向绑定是指当数据发生变化时,视图会自动更新;而当视图发生变化时,数据也会自动更新。 Vue 实现数据双向绑定的方式是通过数据劫持和发布订阅模式相结合。
- 数据劫持:Vue 会拦截 data 对象中所有属性的读取和写入操作。在 Vue 2.x 版本中,数据劫持是通过 Object.defineProperty() 方法实现的;而在 Vue 3.x 版本中,数据劫持则是通过 Proxy 对象实现的。
- 发布订阅模式:当我们修改 data 中的某个属性时,Vue 会通知所有订阅了该属性变化的观察者(Watcher),并执行相应的回调函数。这些回调函数通常会更新视图,以保证视图与数据保持同步。
虚拟 DOM 是一种用 JavaScript 对象表示 DOM 的技术。它可以让我们在不直接操作 DOM 的情况下更新视图。Vue 在更新视图时会先生成一个新的虚拟 DOM 树,然后将新旧虚拟 DOM 树进行对比,找出它们之间的差异。最后,Vue 会根据这些差异来更新真实的 DOM 树。这个过程被称为“patching”。
使用虚拟DOM有以下几个好处:
- 提高渲染性能:直接操作真实DOM通常是非常慢的,因为浏览器需要执行很多额外的工作,如样式计算、布局和重绘。使用虚拟DOM可以减少对真实DOM的操作次数,从而提高渲染性能。
- 跨平台:虚拟DOM是一个抽象层,它可以运行在任何支持JavaScript的平台上。这意味着你可以使用Vue来构建跨平台应用,如桌面应用、移动应用和Web应用。
- 更容易测试:由于虚拟DOM是一个纯粹的数据结构,它更容易进行测试和调试。
相对于手动操作真实DOM,使用虚拟DOM通常可以获得更好的性能。但这并不是绝对的,因为虚拟DOM也有一些开销,如创建虚拟DOM树和计算差异。在某些情况下,手动操作真实DOM可能会更快。但总体来说,使用虚拟DOM可以让我们更容易地构建高性能和跨平台的应用。
生命周期
Vue 的生命周期指的是 Vue 实例从创建到销毁的整个过程。在这个过程中,Vue 实例会经历一系列的生命周期钩子函数,这些钩子函数可以让我们在特定的时刻执行特定的操作。
- beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
created:在实例创建完成后被立即调用。此时,实例已完成以下配置:数据观测、属性和方法的运算、watch/event 事件回调。但是,挂载阶段还没开始,$el 属性目前不可见。 - beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。
mounted:在 el 被新创建的 vm.el 也在文档内。 - beforeUpdate:在数据更新之前调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM。
updated:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。 - beforeDestroy:在实例销毁之前调用。此时实例仍然完全可用。
destroyed:在实例销毁之后调用。此时,所有的指令绑定都被解除,所有的事件监听器都被移除,所有的子实例也都被销毁。
Vuex
Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。使用Vuex时,每一个Vuex应用的核心就是store(仓库)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它可以帮助我们管理共享状态,解决多组件数据通信问题。
简单来说,Vuex就像一个容器,它包含了你的应用中大部分的状态。当Vue组件从store中读取状态时,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
你可以通过store.state来获取状态对象,并通过store.commit方法触发状态变更。在Vue组件中,可以通过this.$store访问store实例,但不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交mutation,而非直接改变store.state.count。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
Vuex主要包括以下几个核心模块:
- State:Vuex使用单一状态树,用一个对象就包含了全部的应用层级状态。每个应用将仅仅包含一个store实例。单一状态树让我们能够直接定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
- Getter:有时候我们需要从store中的state中派生出一些状态,例如对列表进行过滤并计数。Vuex允许我们在store中定义getter(可以认为是store的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- Mutation:更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数。
- Action:Action类似于mutation,不同在于:Action提交的是mutation,而不是直接变更状态;Action可以包含任意异步操作。Action函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。
- Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。为了解决这个问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。
一些常见的Vuex使用场景包括:用户的个人信息管理模块、电商项目的购物车模块、我的订单模块(订单列表中点击取消订单,然后更新对应的订单列表)、在订单结算页获取需要的优惠券并更新订单优惠信息等。
示例:
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
}
})
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
// App.vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count
}
},
methods: {
increment() {
this.$store.dispatch('increment')
}
}
}
</script>
组件之间的通信方式
除了以上说的 Vuex 进行组件之间的通讯外,常见的组件通讯还有以下几种方式:
- props / emit向父组件传递数据。
示例:
// 父组件
<template>
<div>
<child :msg="msg" @changeMsg="changeMsg"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
msg: 'Hello'
}
},
methods: {
changeMsg(newMsg) {
this.msg = newMsg
}
}
}
</script>
// 子组件
<template>
<div>
<p>{{ msg }}</p>
<button @click="changeMsg">Change Msg</button>
</div>
</template>
<script>
export default {
props: ['msg'],
methods: {
changeMsg() {
this.$emit('changeMsg', 'Hi')
}
}
}
</script>
- ref / refs获取子组件的实例,从而调用子组件的方法或访问子组件的数据。
示例:
// 父组件
<template>
<div>
<child ref="child"></child>
<button @click="getChildMsg">Get Child Msg</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
methods: {
getChildMsg() {
console.log(this.$refs.child.msg)
}
}
}
</script>
// 子组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello'
}
}
}
</script>
- eventBus事件总线(on):可以创建一个空的Vue实例作为事件总线,在组件中通过on监听事件,从而实现组件间通信。
示例:
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// 组件A
<template>
<div>
<button @click="emitEvent">Emit Event</button>
</div>
</template>
<script>
import { eventBus } from './eventBus.js'
export default {
methods: {
emitEvent() {
eventBus.$emit('myEvent', 'Hello')
}
}
}
</script>
// 组件B
<template>
<div>{{ msg }}</div>
</template>
<script>
import { eventBus } from './eventBus.js'
export default {
data() {
return {
msg: ''
}
},
mounted() {
eventBus.$on('myEvent', (data) => {
this.msg = data
})
}
}
</script>
- children:子组件可以通过children访问子组件实例。
示例:
// 父组件
<template>
<div>
<child></child>
<button @click="getChildMsg">Get Child Msg</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
methods: {
getChildMsg() {
console.log(this.$children[0].msg)
}
}
}
</script>
// 子组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello'
}
}
}
</script>
- listeners:listeners包含了父组件中的v-on事件监听器。
示例:
// 父组件
<template>
<div id="app">
<middle :msg="msg" @changeMsg="changeMsg"></middle>
</div>
</template>
<script>
import Middle from './Middle.vue'
export default {
components: { Middle },
data() {
return {
msg: 'Hello'
}
},
methods: {
changeMsg(newMsg) {
this.msg = newMsg
}
}
}
</script>
// 中间组件
<template>
<div>
<child v-bind="$attrs" v-on="$listeners"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
inheritAttrs: false // 不继承父组件的属性,避免将属性绑定到根元素上。
}
</script>
// 子组件
<template>
<div>
<p>{{ msg }}</p>
<button @click="changeMsg">Change Msg</button>
</div>
</template>
<script>
export default {
props: ['msg'],
methods: {
changeMsg() {
this.$emit('changeMsg', 'Hi')
}
}
}
</script>
- provide/inject:祖先组件通过provide提供变量,然后在子孙组件中通过inject来注入变量。
示例:
// 祖先组件
<template>
<div>
<child></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
provide() {
return {
msg: 'Hello'
}
}
}
</script>
// 子孙组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
inject: ['msg']
}
</script>
computed与watch
computed和watch都是Vue实例的选项,用来监听数据变化并执行相应的操作。
computed
computed:计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。计算属性默认只有getter,不过在需要时你也可以提供一个setter。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
computed: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
})
watch
watch:当你需要在数据变化时执行异步或开销较大的操作时,可以使用watch。watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
watch: {
message: function (newVal, oldVal) {
console.log('message changed from', oldVal, 'to', newVal)
}
}
})
区别:
- 计算属性是基于它们的依赖进行缓存的。 只有在相关依赖发生改变时,计算属性才会重新求值。这意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,watch选项中的函数每次都会执行。
- 计算属性通常用来计算一个值, 这个值是基于它的依赖进行计算的。当你需要根据数据变化来改变数据时,可以使用计算属性。相比之下,watch选项通常用来执行异步操作或开销较大的操作。
- 计算属性是响应式的, 当它们的依赖发生改变时,它们会自动更新。相比之下,watch选项需要手动设置监听的数据。
总之,当你需要根据数据变化来改变数据时,可以使用计算属性;当你需要根据数据变化来执行异步操作或开销较大的操作时,可以使用watch。
其他
v-if和v-for同时使用在一个元素上的问题
不建议在同一元素上同时使用v-for和v-if。当它们同时存在时,v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个循环的项上。这可能会导致性能问题,因为在渲染列表时会进行更多的计算。
场景一:如果你想根据条件过滤列表并渲染过滤后的结果,可以将过滤后的结果计算为一个计算属性,然后在v-for中使用这个计算属性:
<template>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1', show: true },
{ id: 2, text: 'Item 2', show: false },
{ id: 3, text: 'Item 3', show: true }
]
}
},
computed: {
filteredItems() {
return this.items.filter(item => item.show)
}
}
}
</script>
场景二:如果你的目的是有条件地跳过循环的执行,那么可以将v-if放置在外层元素(如<template>)或包装元素上:
<template>
<ul v-if="shouldShowItems">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
],
shouldShowItems: true
}
}
}
</script>
Vue.nextTick的原理及实现
Vue.nextTick是一个全局API,用于在下一次DOM更新循环结束之后延迟执行一个回调函数。它的实现依赖于JavaScript的事件循环和微任务队列。
- 在Vue 2.x中,Vue.nextTick的实现使用了一个异步队列来存储所有等待执行的回调函数。当一个回调函数被传递给Vue.nextTick时,它会被推入这个异步队列中。然后,Vue会使用一个内部函数来异步刷新这个队列,以便在下一次DOM更新循环结束之后执行所有等待的回调函数。
为了异步刷新队列,Vue会尝试使用原生的Promise.then、MutationObserver或setImmediate来实现异步延迟。如果这些方法都不可用,它会退而使用setTimeout(fn, 0)。
- 在Vue 3.x中,Vue.nextTick的实现类似于Vue 2.x,但使用了更现代的API来实现异步延迟。它首先尝试使用原生的Promise.then,如果不可用则退而使用setTimeout(fn, 0)。
示例:
<template>
<div>
<p ref="message">{{ message }}</p>
<button @click="updateMessage">Update</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello'
}
},
methods: {
updateMessage() {
this.message = 'Updated'
console.log(this.$refs.message.textContent) // => 'Hello'
this.$nextTick(() => {
console.log(this.$refs.message.textContent) // => 'Updated'
})
}
}
}
</script>
总之,Vue.nextTick的实现依赖于JavaScript的事件循环和微任务队列。它使用一个异步队列来存储所有等待执行的回调函数,并使用原生API或setTimeout来异步刷新这个队列。
组件中data是一个函数的原因
在Vue组件中,data必须是一个函数,而不是一个对象。这是因为当一个组件被多次使用时,每个实例都应该维护一份被返回对象的独立的拷贝。
如果data是一个对象,那么所有组件实例将共享同一个数据对象。这意味着当一个组件实例改变了数据对象时,其他组件实例的数据也会受到影响。
为了避免这个问题,Vue要求组件的data选项必须是一个函数。当一个组件被实例化时,Vue会调用这个函数来获取组件的初始数据。由于每个组件实例都会调用这个函数来获取自己的数据,所以每个组件实例都会维护一份独立的数据拷贝。
前端路由
前端路由是指在单页应用(SPA)中,通过改变URL并不向服务器发送请求,而是通过JavaScript来控制页面内容的切换。这种方式可以让用户在不离开当前页面的情况下,浏览不同的内容。
前端路由通常有两种实现方式:hash模式和history模式。
- hash模式:在这种模式下,URL中的hash(即#符号后面的部分)用来表示路由状态。当hash发生变化时,浏览器不会向服务器发送请求,而是触发hashchange事件。我们可以监听这个事件,并根据新的hash值来更新页面内容。这种方式兼容性好,但URL中会多出一个#符号,可能会影响美观。
- history模式:在这种模式下,我们使用HTML5的History API来控制URL的变化。当URL发生变化时,浏览器不会向服务器发送请求,而是触发popstate事件。我们可以监听这个事件,并根据新的URL来更新页面内容。这种方式可以让URL看起来更像传统的URL,但需要服务器端的支持。
Vue diff算法
Vue的diff算法是用来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM的过程。它采用了深度优先遍历和双端比较的策略来优化比较过程,是Vue虚拟DOM实现的核心部分。
Vue的diff算法基于两个假设:
- 两个相同标签的元素会产生类似的DOM结构。
- 同一层级的一组子节点,它们可以通过唯一的id进行区分。
基于这两个假设,Vue的diff算法采用了深度优先遍历和双端比较的策略来比较新旧虚拟DOM树。
在比较过程中,Vue会从新旧虚拟DOM树的根节点开始,逐层进行比较。当遇到不同类型的节点时,Vue会直接替换整个节点及其子节点;当遇到相同类型但属性不同的节点时,Vue会更新节点的属性;当遇到相同类型且属性相同但子节点不同的节点时,Vue会递归地比较子节点。
在比较子节点时,Vue会使用双端比较的策略来优化比较过程。它会同时从新旧虚拟DOM树的两端开始比较,如果发现两端的节点相同,则直接移动节点;如果发现两端的节点不同,则继续比较中间部分。这种策略可以有效地减少需要比较的节点数量,从而提高diff算法的性能。
那么我们常常在for循环中要绑定一个key属性值,有什么作用呢?
其实在Vue中,key是一个特殊的属性,用于标识列表渲染中每个节点的唯一性。这是因为在列表渲染中,列表数据可能会发生变化,导致列表项的顺序、数量或内容发生变化。如果没有key属性,Vue将无法准确地确定新旧虚拟DOM树中的节点是否相同,从而无法快速地更新虚拟DOM树。所以它可以帮助Vue更快地更新虚拟DOM树,从而提高应用的性能。
当Vue进行列表渲染时,它需要一种方式来确定新旧虚拟DOM树中的节点是否相同。如果没有key属性,Vue会默认使用“就地更新”的策略,即直接复用旧虚拟DOM树中的节点来更新新虚拟DOM树中的节点。这种方式简单快速,但在某些情况下可能会导致问题。
为了避免这些问题,我们可以使用key属性来为每个节点指定一个唯一的标识。当Vue进行列表渲染时,它会根据key属性来确定新旧虚拟DOM树中的节点是否相同。这样,Vue就可以更快地更新虚拟DOM树,从而提高应用的性能。
keep-alive使用及原理。
keep-alive是Vue的一个内置组件,用于保留组件状态或避免重新渲染。它可以将其包裹的组件缓存起来,当组件切换时不会销毁,而是保留在内存中,以便下次切换回来时可以直接使用。
实现原理:是通过一个缓存对象来存储被缓存的组件实例。当一个组件被切换出去时,它不会被销毁,而是被保存在缓存对象中;当一个组件被切换回来时,keep-alive会先检查缓存对象中是否有这个组件的实例,如果有,则直接使用缓存的实例;如果没有,则创建一个新的实例。
示例:
<template>
<div>
<button @click="toggle">Toggle</button>
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</div>
</template>
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: {
Foo,
Bar
},
data() {
return {
currentView: 'Foo'
}
},
methods: {
toggle() {
this.currentView = this.currentView === 'Foo' ? 'Bar' : 'Foo'
}
}
}
</script>
插槽
插槽(Slot)是Vue的一个功能,用于实现组件的内容分发。它允许你在父组件中定义一些内容,然后将这些内容分发到子组件的指定位置。
默认插槽,具名插槽和匿名插槽:
- 默认插槽用于分发没有指定名称的内容
- 具名插槽用于分发指定名称的内容。
- 匿名插槽是指没有被元素包裹的内容。
示例:
// 子组件
Vue.component('my-component', {
template: `
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
// 父组件
new Vue({
el: '#app',
template: `
<my-component>
<template v-slot:header>
<h1>Header</h1>
</template>
<p>Content</p>
<template v-slot:footer>
<h1>Footer</h1>
</template>
</my-component>
`
})