Vue中slot的使用详解

 更新时间:2022年05月23日 12:00:15   作者:perryhuan9  
这篇文章主要介绍了Vue中slot的使用详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

在Vue中,我们使用组件来组织页面和组织代码,类似于搭积木,每一个组件都是一个积木,使用一些相同或者不同组件就能搭建出我们想要的页面。

slot(插槽)是组件功能的重要组成部分,插槽必须用于组件才有意义。

它为组件提供了对外的接口,允许从组件外部传递内容,并将这部分内容放置到指定的位置。

使用 slot

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

基础用法

Link.vue

<template>
  <a :href="href" rel="external nofollow"  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" rel="external nofollow" > 百度</Link>
    <br />
    <Link href="https://google.com" rel="external nofollow"  style="margin-top: 10px">
      <!-- 这里允许放置任意的内容,包括字符串和标签 -->
      <span>Icon</span>谷歌</Link
    >
  </div>
</template>
<script>
import Link from "./Link.vue";

export default {
  components: {
    Link,
  },
};
</script>

视觉效果:

image.png

以上实现了两个组件Link.vue和App.vue,Link.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>

效果图:

image.png

作用域插槽

为啥叫作用域插槽?首先要搞清楚作用域这个概念。在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>

效果图:

image.png

从上面的效果图中可以看到,在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代码。

image.png

这个编译是在加载.vue文件的时候就执行了,runtime阶段是不存在模板字符串了(使用UMD的时候会存在),在浏览器中执行的都是编译之后的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})这一句显然是执行_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的实现原理:

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

总结

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Vue-cli3简单使用(图文步骤)

    Vue-cli3简单使用(图文步骤)

    这篇文章主要介绍了Vue-cli3简单使用(图文步骤),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-04-04
  • vue2从数据变化到视图变化发布订阅模式详解

    vue2从数据变化到视图变化发布订阅模式详解

    这篇文章主要为大家介绍了vue2从数据变化到视图变化发布订阅模式详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • vue实现高德地图添加多个点标记

    vue实现高德地图添加多个点标记

    地图多点标注其实是个非常简单的问题,这篇文章主要给大家介绍了关于vue实现高德地图添加多个点标记的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-06-06
  • vue中正确使用jsx语法的姿势分享

    vue中正确使用jsx语法的姿势分享

    这篇文章主要给大家介绍了关于vue中正确使用jsx的相关资料,JSX就是Javascript和XML结合的一种格式,React发明了JSX,利用HTML语法来创建虚拟DOM,当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析,需要的朋友可以参考下
    2021-07-07
  • Vue2递归组件实现树形菜单

    Vue2递归组件实现树形菜单

    这篇文章主要为大家详细介绍了Vue2递归组件实现树形菜单的相关代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • Vue3 响应式 API 及 reactive 和 ref 的用法示例详解

    Vue3 响应式 API 及 reactive 和 ref&

    响应式是一种允许以声明式的方式去适应变化的编程范例,这篇文章主要介绍了关于Vue3响应式API及reactive和ref的用法,需要的朋友可以参考下
    2023-06-06
  • Element el-upload上传组件使用详解

    Element el-upload上传组件使用详解

    本文主要介绍了Element el-upload上传组件使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • vue中如何去掉空格的方法实现

    vue中如何去掉空格的方法实现

    这篇文章主要介绍了vue中如何去掉空格的方法实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • vue基于el-table拓展之table-plus组件

    vue基于el-table拓展之table-plus组件

    本文主要介绍了vue基于el-table拓展之table-plus组件,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • Vue2与Vue3兄弟组件通讯bus的区别及用法

    Vue2与Vue3兄弟组件通讯bus的区别及用法

    这篇文章主要介绍了Vue2与Vue3兄弟组件通讯bus的区别及用法,有需要的朋友可以借鉴参考下,希望可以有所帮助,祝大家多多进步早日升职加薪
    2021-09-09

最新评论