Vue3.0的插槽是如何实现的?

Vue提供了pro可以进行参数的传递,但是有时需要给子组件的模板进行定制化,此时传递参数有时候就不太方便了。 Vue借鉴了Web Components实现了插槽slot

插槽slot通过在父组件中编写DOM,在子组件渲染的时候将这些DOM放置在对应的位置,从而实现内容的分发。

使用方法介绍
基本使用
<Son>
  <p>父组件传入的内容</p>
</Son>

我们想将一些内容渲染在Son子组件中,我们在组件中间写了一些内容,例如<p>父组件传入的内容</p>,但是最终这些内容会被Vue抛弃,是不会被渲染出来的。

如果我们想将<p>父组件传入的内容</p>这部分内容在子组件中渲染,则需要使用slot了。

<!-- Son.vue -->
<div class="card">
  <slot></slot>
</div>

我们只需要在Son组件模板中加入<slot></slot>标签,则<p>父组件传入的内容</p>将替换<slot></slot>渲染

渲染的结果:

<div class="card">
  <p>父组件传入的内容</p>
</div>
默认内容

有些情况下,如果父组件不传入内容,插槽需要显示默认的内容。这时候只需要在<slot></slot>中放置默认的内容就行:

<!-- Son.vue -->
<div class="card">
  <slot>子组件的默认内容</slot>
</div>
  • 如果父组件不传入插槽内容,则渲染为:
<div class="card">
  子组件的默认内容
</div>
  • 如果父组件传入插槽内容<p>父组件传入的内容</p>,则渲染为:
<div class="card">
  <p>父组件传入的内容</p>
</div>
具名插槽

在有些情况下可能需要多个插槽进行内容的放置, 这时候就需要给插槽一个名字:

<!-- Son.vue -->
<div class="card">
  <slot name="header"></slot> 
  <slot>子组件的默认内容</slot>
  <slot name="footer"></slot>
</div>

我们的例子中有三个插槽,其中headerfooter,还有一个没有给名字,其实它也是有名字的,不写名字它的名字就是default, 等同于<slot name="default">子组件的默认内容</slot>

这时候可以根据名称对每个插槽放置不同的内容:

<Son>
  <p>父组件的内容1</p>
  <p>父组件的内容2</p>
  <template v-slot:header> 外部传入的header </template>
  <template v-slot:footer> 外部传入的footer </template>
</Son>

渲染内容如下:

<div class="card">
  外部传入的header
  <p>父组件的内容1</p>
  <p>父组件的内容2</p>
  外部传入的footer
</div>

v-slot:header包含的内容替换<slot name="header"></slot>;
v-slot:footer包含的内容替换<slot name="footer"></slot>;
其他所有内容都被当成v-slot:default替换<slot></slot>;

插槽作用域

插槽的内容使用到数据,那这个数据来自于于父组件,而不是子组件:

  • 父组件
<!-- parent.vue -->
<Son>
  <p>插槽的name {{ name }}</p>
</Son>

setup() {
  return {
    name: ref("parent"),
  }
},
  • 子组件
<!-- son.vue -->
<div class="card">
  <slot></slot>
</div>

setup() {
  return {
    name: ref("chile"),
  }
},

渲染结果:

<div class="card">
  插槽的name: parent
</div>
作用域插槽

我们刚才提到插槽的数据的作用域是父组件,有时候插槽也需要使用来自于子组件的数据,这时候可以使用作用域插槽。

  • 将数据以pro的形式传递
<slot :pro="name"></slot>
  • 父组件接收pro
<template v-slot:default="pro">
  <p>插槽的name {{ pro.pro }}</p>
</template>

此时渲染的内容:

插槽的name: child
实现原理介绍

分析案例:

<!-- Parent.vue -->
<Son>
  <p>插槽的name {{ name }}</p>
  <template v-slot:header> <p>外部传入的header</p> </template>
  <template v-slot:footer> <p>外部传入的footer</p> </template>
</Son>

<!-- Son.vue -->
<div class="card">
  <slot name="header"></slot>
  <slot>子组件的默认内容</slot>
  <slot name="footer"></slot>
</div>
渲染函数分析
  • parent
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "外部传入的header", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "外部传入的footer", -1 /* HOISTED */)

function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_Son = _resolveComponent("Son")

    return (_openBlock(), _createBlock(_component_Son, null, {
      header: _withCtx(() => [
        _hoisted_1
      ]),
      footer: _withCtx(() => [
        _hoisted_2
      ]),
      default: _withCtx(() => [
        _createElementVNode("p", null, "插槽的name " + _toDisplayString(name), 1 /* TEXT */)
      ]),
      _: 1 /* STABLE */
    }))
  }
}

生成子组件的VNode时传了1个children对象, 这个对象有 headrfooter, default 属性,这 3个属性的值就是对应的DOM

  • son
function render(_ctx, _cache) {
  with (_ctx) {
    const { renderSlot: _renderSlot, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", _hoisted_1, [
      _renderSlot($slots, "header"),
      _renderSlot($slots, "default", {}, () => [
        _hoisted_2
      ]),
      _renderSlot($slots, "footer")
    ]))
  }
}

联系这两个渲染函数我们就可以大概有个猜测:子组件渲染的时候遇到slot这个标签,然后就找对应名字的children对应的渲染DOM的内容,进行渲染。即通过renderSlot会渲染headrfooter, default 这三个插槽的内容。

withCtx的作用
export function withCtx(
  fn: Function,
  ctx: ComponentInternalInstance | null = currentRenderingInstance,
  isNonScopedSlot?: boolean // __COMPAT__ only
) {

  const renderFnWithContext: ContextualRenderFn = (...args: any[]) => {

    const prevInstance = setCurrentRenderingInstance(ctx)
    const res = fn(...args)
    setCurrentRenderingInstance(prevInstance)

    return res
  }

  return renderFnWithContext
}

withCtx的作用封装 返回的函数为传入的fn,重要的是保存当前的组件实例currentRenderingInstance,作为函数的作用域。

保存children到组件实例的slots
  • setupComponent setup组件实例的时候会调用initSlots

setup组件实例是什么作用?如果不知道可以参阅我前面的文章。不想看,可以直接理解为先准备数据的阶段,之后会进行组件渲染。

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  initSlots(instance, children)
}

  • children 保存到 instance.slots
export const initSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren
) => {
  // we should avoid the proxy object polluting the slots of the internal instance
  instance.slots = toRaw(children as InternalSlots)
  def(instance.slots, "__vInternal", 1)
}
renderSlot渲染slot内容对应的VNode
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]

  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创建的VNode是一个类型为Fragmentchildren为对应name的插槽的返回值。

结合前面的withCtx的分析,总结来就是 renderSlot创建的VNode是一个类型为Fragmentchildren为对应name的插槽的内容,但是插槽内的数据的作用域是属于父组件的。

processFragment挂载slot内容对应的DOM
const processFragment = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // 
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

  let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

  if (n1 == null) {
    // 插入两个空文本节点   
    hostInsert(fragmentStartAnchor, container, anchor)
    hostInsert(fragmentEndAnchor, container, anchor)
    
    // 挂载数组子节点
    mountChildren(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    // 更新数组子节点
    patchChildren(
      n1,
      n2,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

processFragment先插入两个空文本节点作为锚点,然后挂载数组子节点。

作用域插槽和默认内容的实现逻辑
// 默认内容
const _hoisted_2 = /*#__PURE__*/_createTextVNode("子组件的默认内容")

// pro
renderSlot($slots, "default", { pro: name }, () => [
  _hoisted_2
])

子组件的数据和默认插槽内容作为renderSlot函数的第3个和第4个参数,进行插槽的内容渲染。

我们再回到 renderSlot函数

/**
 * @param slots 组件VNode的slots
 * @param name  slot的name
 * @param props slot的pro
 * @param fallback 默认的内容
 * @param noSlotted 
 * @returns 
 */
export function renderSlot(
  slots: Slots,
  name: string,
  props: Data = {},
  fallback?: () => VNodeArrayChildren,
  noSlotted?: boolean
): VNode {

  // 从 组件VNode的slots对象中找到name对应的渲染函数
  let slot = slots[name]

  // props作为参数执行渲染函数,这样渲染函数就拿到了子组件的数据
  const validSlotContent = slot && ensureValidVNode(slot(props))
  const rendered = createBlock(
    Fragment,
    { key: props.key || `_${name}` },
    validSlotContent || (fallback ? fallback() : []),
    PatchFlags.STABLE_FRAGMENT
  )
  return rendered
}

renderSlot函数接收pros的参数,将其传入slots对象中找到name对应的渲染函数,这样就能获取到子组件的数据pros了;
fallback 是默认的渲染函数,如果父组件没有传递slot,就渲染默认的DOM。

总结
  1. 父组件渲染的时候生成一些withCtx包含的渲染函数,此时将父组件的实例对象持有在函数内部,,所以数据的作用域是父组件;
  2. 子组件在setupComponent先将这些withCtx包含的渲染函数存储在子组件实例对象的slots上;
  3. 子组件渲染的时候,插槽内容的渲染是先找到slots中对应的withCtx包含的渲染函数,然后传入子组件的pro和默认的渲染DOM内容,最后生成插槽渲染内容的DOM内容。

slot

一句话总结:父组件先编写DOM存在子组件实例对象上,渲染子组件的时候再渲染对应的这部分DOM内容。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3.0中的作用域插槽是一种在父组件中向子组件传递数据的机制。它允许父组件将数据传递给子组件,并在子组件中使用这些数据。作用域插槽的使用方式与普通插槽略有不同。 在Vue3.0中,作用域插槽可以通过使用v-slot指令来定义。通过在父组件中使用`<template v-slot:slotName="slotProps">`的语法,我们可以将数据传递给子组件中的作用域插槽。其中,`slotName`是插槽的名称,`slotProps`是传递给插槽的数据对象。 在子组件中,我们可以通过在插槽的位置使用`slotProps`来访问传递过来的数据。这样,我们就可以在子组件中使用父组件传递的数据进行相应的操作。 举个例子,假设我们有一个父组件和一个子组件,父组件中定义了一个作用域插槽,并向子组件传递了一个名为`message`的数据。那么在子组件中,就可以通过`slotProps.message`来访问这个数据。 总结一下,在Vue3.0中使用作用域插槽,我们可以通过`<template v-slot:slotName="slotProps">`来定义插槽,并在子组件中使用`slotProps`来访问传递过来的数据。这样,我们可以灵活地向子组件传递数据,实现更加复杂的组件交互。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Vue 3 第十九章:组件七(插槽)](https://blog.csdn.net/to_the_Future/article/details/129542601)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值