Vue中slot的使用詳解
在Vue中,我們使用組件來組織頁面和組織代碼,類似于搭積木,每一個組件都是一個積木,使用一些相同或者不同組件就能搭建出我們想要的頁面。
slot(插槽)是組件功能的重要組成部分,插槽必須用于組件才有意義。
它為組件提供了對外的接口,允許從組件外部傳遞內(nèi)容,并將這部分內(nèi)容放置到指定的位置。
使用 slot
當(dāng)一個組件可能被使用至少兩次并且兩次使用內(nèi)容(這里指組件視圖的組成)不同時,插槽才有存在的必要。注意: 本文的代碼都是基于Vue3編寫。
基礎(chǔ)用法
Link.vue
<template> ? <a :href="href" rel="external nofollow" class="link"> ? ? <!-- 留個插槽,外界傳入內(nèi)容放置在這里 --> ? ? <slot></slot> ? </a> </template>
<script> export default { ? props: { ? ? href: { ? ? ? required: true, ? ? ? type: String, ? ? }, ? }, }; </script>
<style lang="less" scoped> .link { ? display: inline-block; ? line-height: 1; ? white-space: nowrap; ? cursor: pointer; ? background: #fff; ? border: 1px solid #dcdfe6; ? color: #606266; ? -webkit-appearance: none; ? text-align: center; ? box-sizing: border-box; ? outline: none; ? margin: 0; ? transition: 0.1s; ? font-weight: 500; ? padding: 12px 20px; ? font-size: 14px; ? border-radius: 4px; } </style>
App.vue
<template> ? <div class="app"> ? ? <Link rel="external nofollow" > 百度</Link> ? ? <br /> ? ? <Link rel="external nofollow" style="margin-top: 10px"> ? ? ? <!-- 這里允許放置任意的內(nèi)容,包括字符串和標(biāo)簽 --> ? ? ? <span>Icon</span>谷歌</Link ? ? > ? </div> </template>
<script> import Link from "./Link.vue"; export default { ? components: { ? ? Link, ? }, }; </script>
視覺效果:
以上實現(xiàn)了兩個組件Link.vue和App.vue,Link.vue是一個鏈接組件,在組件內(nèi)部已經(jīng)定義好了樣式,然后鏈接的內(nèi)容交由外界使用時填充。
在App.vue組件內(nèi)則使用了Link.vue組件兩次,并且兩次傳入的內(nèi)容不同。
具名插槽
上面的Link.vue只要求填充一份內(nèi)容,那么當(dāng)我們需要在組件的好幾個位置都填充不同的內(nèi)容應(yīng)該怎么辦?這時候可以使用具名插槽,就是給組件的每個填充區(qū)域都取個名字,這樣在使用的時候就可以往對應(yīng)名字的那個區(qū)域填充內(nèi)容。
Page.vue
<template> <div class="page"> <header class="page-header"> <slot name="header"></slot> </header> <div class="page-center"> <aside class="page-aside"> <slot name="aside"></slot> </aside> <div class="page-content"> <slot name="content"></slot> </div> </div> <footer class="page-footer"> <slot name="footer"></slot> </footer> </div> </template>
<script> export default { ? setup() { ? ? return {}; ? }, }; </script>
<style lang="less"> body { ? margin: 0; } .page { ? border: 1px solid #333; ? width: 100vw; ? height: 100vh; ? display: flex; ? flex-direction: column; ? &-header { ? ? height: 50px; ? ? border-bottom: 1px solid #333333; ? } ? &-center { ? ? flex: 1; ? ? display: flex; ? } ? &-aside { ? ? width: 150px; ? ? border-right: 1px solid #333333; ? } ? &-content { ? ? flex: 1; ? } ? &-footer { ? ? border-top: 1px solid #333; ? ? height: 30px; ? } } </style>
App.vue
<template> ? <Page style="width: 500px; height: 300px; margin: 30px 30px"> ? ? <template v-slot:header>這是標(biāo)題</template> ? ? <template v-slot:aside>這是側(cè)邊欄</template> ? ? <template v-slot:content>這是內(nèi)容區(qū)域</template> ? ? <template v-slot:footer>這是頁腳</template> ? </Page> ? <Page style="width: 500px; height: 300px; margin: 30px 30px"> ? ? <template v-slot:header> ? ? ? <h2>走過路過</h2> ? ? </template> ? ? <template v-slot:aside> ? ? ? <ul> ? ? ? ? <li>東臨碣石</li> ? ? ? ? <li>以觀滄海</li> ? ? ? </ul> ? ? </template> ? ? <template v-slot:content>這是內(nèi)容區(qū)域</template> ? ? <template v-slot:footer>這是頁腳</template> ? </Page> </template>
<script> import Page from "./Page.vue"; export default { ? components: { ? ? Page, ? }, }; </script>
效果圖:
作用域插槽
為啥叫作用域插槽?首先要搞清楚作用域這個概念。在JS中,作用域表示的是當(dāng)前的執(zhí)行上下文,只有在當(dāng)前作用域中變量才可以被使用。作用域有層次之分,分為父作用域和子作用域,子作用域可以訪問父作用域中的變量,這一層層的往上則形成了作用域鏈。JS中只有全局作用域和函數(shù)作用域,ES6新增了塊級作用域。關(guān)于作用域,這里不再贅言,有需要的同學(xué)可以去MDN作用域查看。
Vue本質(zhì)上還是js,模板最終會被編譯成render函數(shù),每個組件都有一個render函數(shù)。下面先看個例子:
Count.vue
<template> ? <div> ? ? <p>當(dāng)前數(shù)字:{{ count }}</p> ? ? <button @click="onAdd">+</button> ? ? <button @click="onMinus">-</button> ? ? <slot></slot> ? </div> </template>
<script> export default { ? data() { ? ? return { ? ? ? count: 0, ? ? }; ? }, ? methods: { ? ? onAdd() { ? ? ? this.count++; ? ? }, ? ? onMinus() { ? ? ? this.count--; ? ? }, ? }, }; </script>
App.vue
<template> ? <div> ? ? <Count style="border: 1px solid red"> ? ? ? <p>這就是填充Count組件的插槽</p> ? ? ? <p>appCount:{{ appCount }}</p> ? ? ? <p>Count組件中的count變量:{{ count }}</p> ? ? </Count> ? ? <br /> ? ? <button @click="onClick">app add</button> ? </div> </template>
<script> import Count from "./Count.vue"; export default { ? components: { ? ? Count, ? }, ? data() { ? ? return { ? ? ? appCount: 0, ? ? }; ? }, ? methods: { ? ? onClick() { ? ? ? this.appCount++; ? ? }, ? }, }; </script>
效果圖:
從上面的效果圖中可以看到,在App.vue組件中使用Count.vue組件時,在Count.vue組件的插槽中,能夠訪問appCount變量,但是不能訪問Count.vue組件的Count變量,這是為什么呢?理論上,插槽傳入的內(nèi)容最終會插入到Count.vue組件中,那么也應(yīng)該可以訪問Count.vue組件的變量才對?。?/p>
父級模板里的所有內(nèi)容都是在父級作用域中編譯的;子模板里的所有內(nèi)容都是在子作用域中編譯的。
上面的一段引用摘自Vue文檔,這段文字表明了,在App.vue中的一切,包括Count.vue組件的插槽內(nèi)容都是在App.vue組件下編譯的,也就是Count.vue組件的插槽模板可以訪問App.vue組件的所有變量,但不能訪問Count.vue的任意變量。如果我一定要在插槽中訪問Count.vue的count變量呢?這個時候作用域插槽就派上用場了。
作用域插槽允許在組件中對插槽所在的上下文暴露某一些變量,改寫以上的Count.vue組件,
Count.vue
<template> <div> <p>當(dāng)前數(shù)字:{{ count }}</p> <button @click="onAdd">+</button> <button @click="onMinus">-</button> <!-- 把count變量暴露到插槽作用域 --> <slot :count="count"></slot> </div> </template>
<script> export default { data() { return { count: 0, }; }, methods: { onAdd() { this.count++; }, onMinus() { this.count--; }, }, }; </script>
App.vue
<template> <div> <Count style="border: 1px solid red"> <!--Count組件插槽暴露的所有變量都放在 slotProps對象中 --> <template v-slot="slotProps"> <p>這就是填充Count組件的插槽</p> <p>appCount:{{ appCount }}</p> <p>Count組件中的count變量:{{ slotProps.count }}</p> </template> </Count> <br /> <button @click="onClick">app add</button> </div> </template>
<script> import Count from "./Count.vue"; export default { components: { Count, }, data() { return { appCount: 0, }; }, methods: { onClick() { this.appCount++; }, }, }; </script>
這就是作用域插槽,本質(zhì)上了是允許在父組件作用域訪問到子組件作用域,它為插槽模板區(qū)域提供了一個數(shù)據(jù)來源于子組件的上下文。
作用域插槽的用處還是挺廣的,總的來說當(dāng)你需要它時自然會用到它,如果想提前學(xué)習(xí),可以看一下elementUI的table組件。
slot 實現(xiàn)
上面就插槽的使用說了一大堆,關(guān)于插槽的實現(xiàn)還是沒有涉及,下文講解在Vue中插槽是如何實現(xiàn)的?
首先,我們都知道,無論是使用jsx還是模板,最終都會編譯成render函數(shù),并且render函數(shù)在執(zhí)行之后會輸出 Virtual Dom ,下面先看一個組件在編譯完成之后是什么樣子?
Comp.vue
<template> <div> <p>count: {{count}}</p> <button @click="onClick"> ADD </button> <slot :count="count"></slot> </div> </template>
<script> import {defineComponent, ref} from 'vue' export default defineComponent((props) => { const count = ref(0); const onClick = () => { count.value++ } return { count, onClick } }) </script>
App.vue
<template> <div> <Comp> <template v-slot="slotProps"> <p> {{magRef}}: {{slotProps.count}} </p> </template> </Comp> </div> </template>
<script> import {defineComponent, ref} from 'vue' import Comp from './Comp.vue' export default defineComponent({ components: {Comp}, setup(props) { const magRef = ref('當(dāng)前的數(shù)字是') return { magRef } } }) </script>
Comp.vue編譯之后:
/* Analyzed bindings: {} */ import { defineComponent, ref } from 'vue' const __sfc__ = defineComponent((props) => { const count = ref(0); const onClick = () => { count.value++ } return { count, onClick } }) import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, renderSlot as _renderSlot, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("p", null, "count: " + _toDisplayString(_ctx.count), 1 /* TEXT */ ), _createElementVNode("button", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args))) }, " ADD "), _renderSlot(_ctx.$slots, "default", { count: _ctx.count }) ])) } __sfc__.render = render __sfc__.__file = "Comp.vue" export default __sfc__
App.vue編譯之后:
/* Analyzed bindings: {} */ import { defineComponent, ref } from 'vue' import Comp from './Comp.vue' const __sfc__ = defineComponent({ components: { Comp }, setup(props) { const magRef = ref('當(dāng)前的數(shù)字是') return { magRef } } }) import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_Comp = _resolveComponent("Comp") return (_openBlock(), _createElementBlock("div", null, [ _createVNode(_component_Comp, null, { default: _withCtx((slotProps) => [ _createElementVNode("p", null, _toDisplayString(_ctx.magRef) + ": " + _toDisplayString(slotProps.count), 1 /* TEXT */ ) ]), _: 1 /* STABLE */ }) ])) } __sfc__.render = render __sfc__.__file = "App.vue" export default __sfc__
這里給大家推薦一個尤雨溪搞的測試網(wǎng)站Vue SFC Playground 可以直接看到組件編譯之后的js代碼。
這個編譯是在加載.vue文件的時候就執(zhí)行了,runtime階段是不存在模板字符串了(使用UMD的時候會存在),在瀏覽器中執(zhí)行的都是編譯之后的js。下面具體分析一下以上Comp.vue和App.vue編譯之后的js代碼。
首先在Comp.vue中,<slot :count="count"></slot>會被編譯成_renderSlot(_ctx.$slots, "default", {count: _ctx.count}),下面看看_renderSlot中干了什么?
export type Slot = (...args: any[]) => VNode[] export type InternalSlots = { [name: string]: Slot | undefined } export function renderSlot( slots: Slots, name: string, props: Data = {}, // this is not a user-facing function, so the fallback is always generated by // the compiler and guaranteed to be a function returning an array fallback?: () => VNodeArrayChildren, noSlotted?: boolean ): VNode { let slot = slots[name] openBlock() const validSlotContent = slot && ensureValidVNode(slot(props)) const rendered = createBlock( Fragment, { key: props.key || `_${name}` }, validSlotContent || (fallback ? fallback() : []), validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE ? PatchFlags.STABLE_FRAGMENT : PatchFlags.BAIL ) return rendered }
_renderSlot(_ctx.$slots, "default", {count: _ctx.count})這一句顯然是執(zhí)行_ctx.$slots.default({count: _ctx.count}),這說明在父組件中,每個插槽模板最終會被編譯成一個函數(shù),并且這個函數(shù)會被傳遞到子組件,在子組件里面會以props(這里是{count: _ctx.count})作為參數(shù)執(zhí)行插槽函數(shù),最終_ctx.$slots.default({count: _ctx.count})會返回virtual dom對象。
下面再看一下App.vue組件:
<Comp> <template v-slot="slotProps"> <p> {{magRef}}: {{slotProps.count}} </p> </template> </Comp>
被編譯成了:
_createVNode(_component_Comp, null, { default: _withCtx((slotProps) => [ _createElementVNode("p", null, _toDisplayString(_ctx.magRef) + ": " + _toDisplayString(slotProps.count), 1 /* TEXT */ ) ]), _: 1 /* STABLE */ })
請忽略_withCtx,顯然模板會編譯成一個函數(shù),并傳遞到子組件,進而在子組件中構(gòu)建出完整的virtual dom, 上面中_ctx是當(dāng)前組件的上下文,slotProps則是作用域插槽暴露的參數(shù)。
由此可以做一個總結(jié),vue slot的實現(xiàn)原理:
- 所有的模板會被編譯成創(chuàng)建vnode的函數(shù)。
- 父組件中傳遞給子組件的插槽(每個插槽都是一個函數(shù),即名字不同的插槽為不同的函數(shù))內(nèi)容模板也會被編譯成函數(shù)并且傳遞給子組件,模板中如果使用了父組件的變量,那么會通過閉包的形式在插槽函數(shù)中被使用。
- 子組件在接收到父組件傳遞的插槽內(nèi)容函數(shù),會以在slot暴露的變量(只有作用域插槽有這些變量)為參數(shù)執(zhí)行這個函數(shù),返回vnode,這個vnode會作為子組件vnode的一部分。
總結(jié)
本文從使用和實現(xiàn)兩個方面講解了vue slot,有一定的深度,但忽略了一些使用和實現(xiàn)上的細節(jié),有不足之處還請指出且諒解。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue2從數(shù)據(jù)變化到視圖變化發(fā)布訂閱模式詳解
這篇文章主要為大家介紹了vue2從數(shù)據(jù)變化到視圖變化發(fā)布訂閱模式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09Vue3 響應(yīng)式 API 及 reactive 和 ref&
響應(yīng)式是一種允許以聲明式的方式去適應(yīng)變化的編程范例,這篇文章主要介紹了關(guān)于Vue3響應(yīng)式API及reactive和ref的用法,需要的朋友可以參考下2023-06-06