Vue中,我们使用组件来组织页面和组织代码,类似于搭积木,每一个组件都是一个积木,使用一些相同或者不同组件就能搭建出我们想要的页面。slot(插槽)是组件功能的重要组成部分,插槽必须用于组件才有意义。它为组件提供了对外的接口,允许从组件外部传递内容,并将这部分内容放置到指定的位置。

使用 slot

当一个组件可能被使用至少两次并且两次使用内容(这里指组件视图的组成)不同时,插槽才有存在的必要。注意: 本文的代码都是基于Vue3编写

基础用法

Link.vue

<template><a :href="href" class="link"><!-- 留个插槽,外界传入内容放置在这里 --><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 href="https://baidu.com"> 百度</Link><br /><Link href="https://google.com" style="margin-top: 10px"><!-- 这里允许放置任意的内容,包括字符串和标签 --><span>Icon</span>谷歌</Link></div>
</template><script>
import Link from "./Link.vue";export default {components: {Link,},
};
</script>

视觉效果:

以上实现了两个组件Link.vueApp.vueLink.vue是一个链接组件,在组件内部已经定义好了样式,然后链接的内容交由外界使用时填充。在App.vue组件内则使用了Link.vue组件两次,并且两次传入的内容不同。

具名插槽

上面的Link.vue只要求填充一份内容,那么当我们需要在组件的好几个位置都填充不同的内容应该怎么办?这时候可以使用具名插槽,就是给组件的每个填充区域都取个名字,这样在使用的时候就可以往对应名字的那个区域填充内容。

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>这是标题</template><template v-slot:aside>这是侧边栏</template><template v-slot:content>这是内容区域</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>这是内容区域</template><template v-slot:footer>这是页脚</template></Page>
</template><script>
import Page from "./Page.vue";export default {components: {Page,},
};
</script>

效果图:

作用域插槽

为啥叫作用域插槽?首先要搞清楚作用域这个概念。在JS中,作用域表示的是当前的执行上下文,只有在当前作用域中变量才可以被使用。作用域有层次之分,分为父作用域和子作用域,子作用域可以访问父作用域中的变量,这一层层的往上则形成了作用域链。JS中只有全局作用域和函数作用域,ES6新增了块级作用域。关于作用域,这里不再赘言,有需要的同学可以去MDN作用域查看。

Vue本质上还是js,模板最终会被编译成render函数,每个组件都有一个render函数。下面先看个例子:

Count.vue

<template><div><p>当前数字:{{ 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变量,这是为什么呢?理论上,插槽传入的内容最终会插入到Count.vue组件中,那么也应该可以访问Count.vue组件的变量才对啊?

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

上面的一段引用摘自Vue文档,这段文字表明了,在App.vue中的一切,包括Count.vue组件的插槽内容都是在App.vue组件下编译的,也就是Count.vue组件的插槽模板可以访问App.vue组件的所有变量,但不能访问Count.vue的任意变量。如果我一定要在插槽中访问Count.vue的count变量呢?这个时候作用域插槽就派上用场了。

作用域插槽允许在组件中对插槽所在的上下文暴露某一些变量,改写以上的Count.vue组件,

Count.vue

<template><div><p>当前数字:{{ 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>

这就是作用域插槽,本质上了是允许在父组件作用域访问到子组件作用域,它为插槽模板区域提供了一个数据来源于子组件的上下文。作用域插槽的用处还是挺广的,总的来说当你需要它时自然会用到它,如果想提前学习,可以看一下elementUI的table组件。

slot 实现

上面就插槽的使用说了一大堆,关于插槽的实现还是没有涉及,下文讲解在Vue中插槽是如何实现的?

首先,我们都知道,无论是使用jsx还是模板,最终都会编译成render函数,并且render函数在执行之后会输出 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('当前的数字是')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('当前的数字是')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__

这里给大家推荐一个尤雨溪搞的测试网站Vue SFC Playground 可以直接看到组件编译之后的js代码。

这个编译是在加载.vue文件的时候就执行了,runtime阶段是不存在模板字符串了(使用UMD的时候会存在),在浏览器中执行的都是编译之后的js。下面具体分析一下以上Comp.vueApp.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 arrayfallback?: () => 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})这一句显然是执行_ctx.$slots.default({count: _ctx.count}),这说明在父组件中,每个插槽模板最终会被编译成一个函数,并且这个函数会被传递到子组件,在子组件里面会以props(这里是{count: _ctx.count})作为参数执行插槽函数,最终_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,显然模板会编译成一个函数,并传递到子组件,进而在子组件中构建出完整的virtual dom, 上面中_ctx是当前组件的上下文,slotProps则是作用域插槽暴露的参数。

由此可以做一个总结,vue slot的实现原理:

  1. 所有的模板会被编译成创建vnode的函数。
  2. 父组件中传递给子组件的插槽(每个插槽都是一个函数,即名字不同的插槽为不同的函数)内容模板也会被编译成函数并且传递给子组件,模板中如果使用了父组件的变量,那么会通过闭包的形式在插槽函数中被使用。
  3. 子组件在接收到父组件传递的插槽内容函数,会以在slot暴露的变量(只有作用域插槽有这些变量)为参数执行这个函数,返回vnode,这个vnode会作为子组件vnode的一部分。

总结

本文从使用和实现两个方面讲解了vue slot,有一定的深度,但忽略了一些使用和实现上的细节,有不足之处还请指出且谅解。

Vue slot 详解相关推荐

  1. [Vue] slot详解,slot、slot-scope和v-slot

    插槽(solt) 什么是插槽? 插槽就是父组件可以在子组件的指定位置插入html结构 插槽的作用? 让用户可以拓展组件,去更好地复用组件和对其做定制化处理. 举一些例子,比如布局组件.表格列.下拉选项 ...

  2. 七、Vue cli详解学习笔记——什么是Vue cli ,Vue cli的使用(安装,拉取2.x模板,初始化项目),Vue cli2详解,Runtime-Compiler和Runtime-only区别

    一.什么是Vue CLI 如果你只是简单写几个Vue的Demo程序, 那么你不需要Vue CLI. 如果你在开发大型项目, 那么你需要, 并且必然需要使用Vue CLI 使用Vue.js开发大型应用时 ...

  3. Linux运行脚手架vue,Linux Nodejs与vue脚手架详解

    本篇教程介绍了Linux Nodejs与vue脚手架详解,希望阅读本篇文章以后大家有所收获,帮助大家对Node.js的理解更加深入. < https://nodejs.org/dist/v8.9 ...

  4. vue路由详解 --基础

    vue路由详解 --基础 1.router-link 和router-view组件 router-link相当于封装了一个a标签 router-view为组件显示的位置 <router-link ...

  5. Vue实例详解与生命周期

    Vue实例详解与生命周期 http://www.jianshu.com/p/b5858f1e6e76 Vue的实例是Vue框架的入口,其实也就是前端的ViewModel,它包含了页面中的业务逻辑处理. ...

  6. Electron vue使用详解

    Electron  vue使用详解 Electron是什么? Electron 是一个框架,可以让您使用 JavaScript, HTML 和 CSS 创建桌面应用程序. 然后这些应用程序可以打包在m ...

  7. Vue中 Vue.prototype 详解及使用——作用:避免和已被定义的数据、方法、计算属性产生冲突

    Vue中 Vue.prototype 详解及使用--作用:避免和已被定义的数据.方法.计算属性产生冲突 **应用场景:**在很多组件里用到数据/实用工具,但是不想污染全局作用域.这种情况下,可以通过在 ...

  8. 【Vue组件详解(一)】

    Vue组件详解(一) 简介 对组件的理解 传统方式编写页面 组件方式编写页面 非单文件组件 完整代码 总结 简介 组件的定义其实就是实现应用中局部功能代码(html,css,js)和资源(MP3 ,M ...

  9. vue 监控元素宽度_Vue入门系列之Vue实例详解与生命周期

    Vue的实例是Vue框架的入口,其实也就是前端的ViewModel,它包含了页面中的业务逻辑处理.数据模型等,当然它也有自己的一系列的生命周期的事件钩子,辅助我们进行对整个Vue实例生成.编译.挂着. ...

最新文章

  1. 项目经理面试中可能遇到的问题
  2. 802.11 Power Save(节电/省电/节能)机制总结
  3. Vue+ElementUI纯前端技术实现对表格数据的增删改查
  4. mysql查询每个机构下的账号总数_MySQL高可用方案:实践篇
  5. redis 底层数据结构 压缩列表 ziplist
  6. kaili camera
  7. VC2005 C++入门记
  8. 编写javascript、Jquery的String.format();
  9. jquery系列教程5-动画操作全解
  10. NPAPI中返回一个字串的正确做法
  11. 与人和代码打交道,有何不同?
  12. 研磨设计模式之《观察者模式observer》
  13. Foobar2000自用插件
  14. JavaWeb项目打包上线简单流程
  15. EV: 致新教育萤火虫父母们
  16. MAML 源代码解释说明 (一)
  17. 自动驾驶入门技术(4)—— 摄像头
  18. 【每周CV论文推荐】StyleGAN人脸属性编辑有哪些经典论文需要阅读
  19. 高压直流电源系统(直流ups)有哪些特点?
  20. Vundle ,Vim Bundle

热门文章

  1. 计算机毕业设计项目2023推荐
  2. 用环境传感器做一个气象站
  3. java中iterable_java中的Iterator和Iterable
  4. Linux桌面GUI自动化测试工具-----dogtail
  5. redis哨兵模式讲解
  6. 【C++】day03 - 【类型与对象的概念】【类型】【构造函数】【一个对象创建的过程】【构造函数的应用】【头文件和实现文件的分离】【this指针】【析构函数】
  7. 主机字节序与网络字节序的转换函数:htonl、ntohl、htons、ntohs【转】
  8. python的格式转换库_3个Python PDF库,提取信息、转换格式、分割剪裁有它就够了!...
  9. z490 linux raid,光威弈系列Z490平台装机评测(含raid模式)
  10. 时钟(AnalogClock和 DigitalClock)的功能与用法