简述slot

slot插槽是vue对组件嵌套这种扩展机制的称谓,在react可以也这样称呼,但是并不很常见。不过叫slot确实很形象。
这样是形式就是插槽:

  1. <template>
  2. <container-comp>
  3. <content></content>
  4. <footer></footer>
  5. </container-comp>
  6. </template>
  1. () => (
  2. <ContainerComp>
  3. <Content />
  4. <Footer />
  5. </ContainerComp>
  6. )

(我们可以把container-comp称之为容器组件,把content、footer称之为子组件)
这种机制的好处主要在于,在某个容器提供的模版或者数据中,可以根据需求灵活扩展需要渲染的子组件。
专业点说就是通过容器和子组件之间的协议(数据交换和渲染方式),将彼此逻辑独立解藕,提升了各自的复用性。
举个例子:容器组件提供了一份渲染模版,将各个模块的位置预留出来,使用的时候根据各个子组件的顺序或者插槽名称,在不同场景可以选择不同的子组件。
再举个使用插槽的例子:容器组件中提供一些数据,比如定时请求某种接口得到数据,或者监听,订阅一些数据,比如在挂载完成后监听鼠标事件,得到位置数据。数据拿到之后,具体的对数据的渲染方式交给子组件来做,而这时候,容器可以通过参数传递的方式将得到的数据交给子组件。
通过上述的例子描述,可以看到,使用插槽的的代码设计符合单一职责原则,逻辑更加内聚。
而不管是vue还是react上述描述的这些功能都是支持的,只是有的叫法略有所别,但是其目的一致。
(因为近期在项目中使用vue多一点,而之前对vue的了解只是笼统的学习过响应式原理,并没有真正在项目里写过vue,所以,近期希望结合vue和react,来系统的回顾回顾相关的知识点。)
下面就先看下vue中的插槽都有什么功能。一边看vue,一边对比react。
参考这里:,可以看到,插槽相关的核心功能有:

  • 基本渲染插槽内容;
  • 具名插槽:一个容器多个插槽,需要分别命名;
  • 插槽传递参数,属于相对高级点的用法;

    基本插槽

    vue中基本插槽

    最简单插槽用法就像下面这样:

    1. <template>
    2. <main-comp>
    3. <div>内容</div>
    4. <!-- 可以是任何自定义组件
    5. <my-sub></my-sub>
    6. -->
    7. </main-comp>
    8. </template>

    main-comp就是容器组件,而我们将“div内容”作为容器的子元素,那么,容器里面怎么写呢?

    1. <template>
    2. <div class="main">
    3. <slot>后备信息,以防万一</slot>
    4. </div>
    5. </template>

    可以看到,容器里面简单的使用slot标签,相当于占位。当组件渲染的时候,”“ 将会被替换为”

    内容
    “,如果没有使用时插槽的话,会渲染出slot标签内的内容:“后备信息,以防万一”。当然,使用的插槽不止是”
    内容
    “这么简单,可以是任何自定义组件。
    ok,对应react中实现对应的代码怎么写呢?

    react中基本插槽

    1. () => (
    2. <MainComp>
    3. <div>内容</div>
    4. {/* <MySub /> 可以是任何自定义组件 */}
    5. </MainComp>
    6. )
    1. const MainComp = (props) => {
    2. return (
    3. <div class="main">
    4. {props.children ?? '后备信息,以防万一'}
    5. </div>
    6. )
    7. }

    react中,组件的子组件都存在props.children中,所以直接在return中对应的位置渲染props.children就好,而后备内容可以应用任何js语法,来判断props.children收否存在从而显示后备内容与否。

    具名插槽

    vue具名插槽

    上面是最简单的场景,但有时候,一个容器需要渲染很多插槽,比如需要渲染一个内容区域content和一个底部区域footer,这时候就需要对插槽命名了,称之为具名插槽:

    1. <template>
    2. <main-comp>
    3. <template v-slot:content>
    4. <sub-comp1></sub-comp1>
    5. </template>
    6. <template v-slot:footer>
    7. <footer-comp></footer-comp>
    8. </template>
    9. </main-comp>
    10. </template>

    定义插槽:

    1. <template>
    2. <div>
    3. <slot name="content"></slot>
    4. <slot name="footer"></slot>
    5. </div>
    6. </template>

    slot用name属性标注了其名字xxx,对应使用的时候要用v-slot:xxx,这样xxx的template就会对应替换成name是xxx的slot的位置。有一点需要注意,v-slot这个指令对template生效,也就是说,要使用具名插槽,必须要用template将内容包裹起来。
    另外,如果slot显示指定name,其实它对应也是有name的,它的默认name叫default。

    关于react具名插槽的讨论

    对应react的话,我用了这些年react,还真没听过“具名插槽”这个称谓。不过尽管没有100%一致的对应vue的具名插槽,但类似的功能有几种实现方式:

    react模仿具名插槽

    a. 我们知道react的“插槽”写法(嵌套子组件),子组件都是作为props.children数组的子元素,那么其实最简单的一种方式就是,children的次序对应着某个子插槽。比如:
    使用时:

    1. () => {
    2. return (
    3. <MainComp>
    4. <SubComp1 />
    5. <FooterComp />
    6. </MainComp>
    7. )
    8. }

    那么,props.children[0]对应的就是SubComp1,而props.children[1]对应的就是FooterComp,所以在MainComp内部就可以这样:

    1. const MainComp = (props) => {
    2. const content = props.children[0]
    3. const footer = props.children[1]
    4. return (
    5. <div>
    6. { content }
    7. { footer }
    8. </div>
    9. )
    10. }

    但是上述写法的问题在于:具名呢?说好的名称呢?顺序乱了咋办?
    b. 想要实现具名,可以下面这样写法,本质还是props.children,我们把对象作为“插槽”内容,对象的key就是插槽名称,value就是子组件:

    1. () => {
    2. return (
    3. <MainComp>
    4. {
    5. {
    6. content:(<SubComp1 />),
    7. footer:(<FooterComp />)
    8. }
    9. }
    10. </MainComp>
    11. )
    12. }

    对应的MainComp:

    1. const MainComp = (props) => {
    2. const { content, footer } = props.children
    3. return (
    4. <div>
    5. { content }
    6. { footer }
    7. </div>
    8. )
    9. }

    这里这个props.children可以直接解构对象的属性。(props.children不一定是数组,当只有一个元素的时候就不是数组)。
    c. 上述这种写法看上去和vue的功能一致了,但是坦白讲,这样的代码在react世界里面实属罕见。不是说错,但是似乎不那么符合使用习惯。
    react中类似具名的插槽其实还可以通过给子组件显示命名方式实现,其实就是给子组件挂了个静态变量: ```jsx const Content = () => (

    I am Content
    )

const Footer = (props) => (

Footer Here {props.info}
)

// 两个子组件标记出来 Content.compName = ‘content’ Footer.compName = ‘footer’

  1. 这样在容器组件中就能通过这两个标记,识别出对应的组件:
  2. ```jsx
  3. export function MainComp(props) {
  4. const isMany = React.Children.count(props.children) > 0
  5. let footer = (<div>footer</div>)
  6. let content = (<div>content</div>)
  7. if (isMany) {
  8. React.Children.forEach(props.children, item => {
  9. const { compName } = item.type
  10. // 判断子组件类型
  11. if (compName === 'footer') footer = item
  12. if (compName === 'content') content = item
  13. })
  14. }
  15. return (
  16. <div>
  17. title text<br/>
  18. { content }
  19. { footer }
  20. </div>
  21. )
  22. }

这里面用了几个React.Children的方法来判断props.children,核心逻辑是当children是数组时,变量每一个子项,判断其“具名”(这里我们的约定为compName),设置对应的需要渲染的子组件的变量。
isMany部分也可以这样写:

  1. try {
  2. React.Children.only(children)
  3. } catch (e) {
  4. React.Children.forEach(children, item => {
  5. const { compName } = item.type
  6. if (compName === 'footer') footer = item
  7. if (compName === 'content') content = item
  8. })
  9. }

这样我们在使用“插槽”组件的时候就不用担心子组件次序问题了:

  1. () => {
  2. return (
  3. <MainComp>
  4. <Footer /> {/* 先写footer还是能正确的渲染出来 */}
  5. <Content />
  6. </MainComp>
  7. )
  8. }

上面a、b、c三种方法实现了和vue一样的具名的slot,第一种只是简单的次序对应,第二种能实现,但是不太符合react习惯,第三种能实现,也是react的写法。
不过,一般简单的需求,没必要用第三种,可以直接使用属性传递组件,我也不知道应该怎么叫,就叫属性插槽吧。

属性插槽:

直接传递props,只不过props是个组件:

  1. () => {
  2. return (
  3. <MainComp
  4. content={(<SubComp1 />)}
  5. footer={(<FooterComp />)}
  6. />
  7. )
  8. }

这样在MainComp的内部直接通过props去拿对应的组件并渲染在适当的位置就行:

  1. const MainComp = (props) => {
  2. const { content, footer } = props
  3. return (
  4. <div>
  5. { content }
  6. { footer }
  7. </div>
  8. )
  9. }

严格意义上说,虽然这样能实现和vue具名插槽一样的功能,但使用却不是用插槽的形式。
但是这样代码在react世界中,却是最常见的方式。这可能是两种框架不同特点导致的微小差异了。

插槽传参

vue插槽传参

插槽传递参数,可以帮助我们实现一些更高级的功能。
先看下vue中,如何给插槽传递参数:

  1. <template>
  2. <div class="main">
  3. <slot
  4. :styleProps="shareStyle"
  5. :data="shareStyle"
  6. :description="desc">
  7. </slot>
  8. </div>
  9. </template>

上面的代码是定义slot时候,我们给slot绑定了三个属性,这看上去和我们使用组件,给组件传递参数的用法没有什么区别。
使用slot的时候这样接收参数:

  1. <!-- slotProps 是通过main传递过来的 -->
  2. <template v-slot:default="slotProps">
  3. <!-- 默认插槽可以简写成 v-slot -->
  4. <div :style="slotProps.styleProps">{{ slotProps.description}}</div>
  5. </template>

首先我们看到使用的时候在template标签中使用了指令v-slot,即v-slot:default=”slotProps”,这句话什么意思呢?就是当前这里template对应default这个名称的slot(defalut可以省略,上面定义slot的地方也没写name=“default”,其实是省略了)。
而这个slotProps表示所有传递过来的属性,也就是说:

  1. slotProps = {
  2. styleProps, data, description
  3. }

所以在template中,可以使用slotProps上的任何属性。也可以给作为插槽的自定义组件传递参数, 比如:

  1. <template v-slot:rightItem="slotProps">
  2. <staff :staff="slotProps.data"></staff>
  3. </template>

vue插槽传参就是这样,看看react吧。

react的render-props

react中其实没法直接给插槽传递参数,只能借助一点技术手段:函数。
这种方式有个专有名词叫:render-props。
具体的方式就是,子组件作为插槽是用函数的形式,而容器组件渲染的时候对应的就调用这个函数,在调用函数的时候,把需要传递的参数传入函数,这样在插槽函数的作用域内就拿到了数据:

  1. () => {
  2. return (
  3. <MainComp>
  4. {
  5. (data) => (<Staff staff={data} />)
  6. }
  7. </MainComp>
  8. )
  9. }

看下容器MainComp组件:

  1. const MainComp = (props) => {
  2. const [data, setData] = useState({})
  3. useEffect(() => {
  4. const info = await getData()
  5. setData(info)
  6. }, [])
  7. return (
  8. <div>
  9. {
  10. props.children && props.children(data)
  11. }
  12. </div>
  13. )
  14. }

当然了,这种函数形式的“插槽”不止可以用作插槽,用在普通的props上自然也是可以的,在react世界里,凡要从另一个组件拿数据的场合,都可以考虑传个函数:

  1. () => {
  2. return (
  3. <MainComp
  4. staff={(data) => (<Staff staff={data} />)}
  5. />
  6. )
  7. }

对应的MainComp,最终也是通过函数调用给子组件传递参数,只是获取子组件的方式换一下:

  1. const MainComp = (props) => {
  2. const [data, setData] = useState({})
  3. useEffect(() => {
  4. const info = await getData()
  5. setData(info)
  6. }, [])
  7. return (
  8. <div>
  9. {/* 这里变一下 */}
  10. {
  11. props.staff && props.staff(data)
  12. }
  13. </div>
  14. )
  15. }

以上就是对两种框架“插槽”相关的实现方式的简单总结。
总言之,不管vue或者react,都有着很灵活的用法,上面的这些要素和技巧,都可以在实际项目中可以根据需要自行组合或者扩展。