Vue3.0的插槽是如何實現(xiàn)的?

Vue提供了pro可以進行參數(shù)的傳遞,但是有時需要給子組件的模板進行定制化,此時傳遞參數(shù)有時候就不太方便了。 Vue借鑒了Web Components實現(xiàn)了插槽slot。

插槽slot通過在父組件中編寫DOM,在子組件渲染的時候將這些DOM放置在對應的位置,從而實現(xiàn)內容的分發(fā)。

使用方法介紹

基本使用
<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>

我們的例子中有三個插槽,其中header,footer,還有一個沒有給名字,其實它也是有名字的,不寫名字它的名字就是default, 等同于<slot name="default">子組件的默認內容</slot>。

這時候可以根據(jù)名稱對每個插槽放置不同的內容:

<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>;

插槽作用域

插槽的內容使用到數(shù)據(jù),那這個數(shù)據(jù)來自于于父組件,而不是子組件:

  • 父組件
<!-- 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>
作用域插槽

我們剛才提到插槽的數(shù)據(jù)的作用域是父組件,有時候插槽也需要使用來自于子組件的數(shù)據(jù),這時候可以使用作用域插槽。

  • 將數(shù)據(jù)以pro的形式傳遞
<slot :pro="name"></slot>
  • 父組件接收pro
<template v-slot:default="pro">
  <p>插槽的name {{ pro.pro }}</p>
</template>

此時渲染的內容:

插槽的name: child

實現(xiàn)原理介紹

分析案例:

<!-- 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>
渲染函數(shù)分析
  • 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對象, 這個對象有 headr,footer, 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")
    ]))
  }
}

聯(lián)系這兩個渲染函數(shù)我們就可以大概有個猜測:子組件渲染的時候遇到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的作用封裝 返回的函數(shù)為傳入的fn,重要的是保存當前的組件實例currentRenderingInstance,作為函數(shù)的作用域。

保存children到組件實例的slots
  • setupComponent setup組件實例的時候會調用initSlots

setup組件實例是什么作用?如果不知道可以參閱我前面的文章。不想看,可以直接理解為先準備數(shù)據(jù)的階段,之后會進行組件渲染。

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創(chuàng)建的VNode是一個類型為Fragmentchildren為對應name的插槽的返回值。

結合前面的withCtx的分析,總結來就是 renderSlot創(chuàng)建的VNode是一個類型為Fragment,children為對應name的插槽的內容,但是插槽內的數(shù)據(jù)的作用域是屬于父組件的。

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) {
    // 插入兩個空文本節(jié)點   
    hostInsert(fragmentStartAnchor, container, anchor)
    hostInsert(fragmentEndAnchor, container, anchor)
    
    // 掛載數(shù)組子節(jié)點
    mountChildren(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    // 更新數(shù)組子節(jié)點
    patchChildren(
      n1,
      n2,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

processFragment先插入兩個空文本節(jié)點作為錨點,然后掛載數(shù)組子節(jié)點。

作用域插槽和默認內容的實現(xiàn)邏輯
// 默認內容
const _hoisted_2 = /*#__PURE__*/_createTextVNode("子組件的默認內容")

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

子組件的數(shù)據(jù)和默認插槽內容作為renderSlot函數(shù)的第3個和第4個參數(shù),進行插槽的內容渲染。

我們再回到 renderSlot函數(shù)

/**
 * @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對應的渲染函數(shù)
  let slot = slots[name]

  // props作為參數(shù)執(zhí)行渲染函數(shù),這樣渲染函數(shù)就拿到了子組件的數(shù)據(jù)
  const validSlotContent = slot && ensureValidVNode(slot(props))
  const rendered = createBlock(
    Fragment,
    { key: props.key || `_${name}` },
    validSlotContent || (fallback ? fallback() : []),
    PatchFlags.STABLE_FRAGMENT
  )
  return rendered
}

renderSlot函數(shù)接收pros的參數(shù),將其傳入slots對象中找到name對應的渲染函數(shù),這樣就能獲取到子組件的數(shù)據(jù)pros了;
fallback 是默認的渲染函數(shù),如果父組件沒有傳遞slot,就渲染默認的DOM。

總結

  1. 父組件渲染的時候生成一些withCtx包含的渲染函數(shù),此時將父組件的實例對象持有在函數(shù)內部,,所以數(shù)據(jù)的作用域是父組件;
  2. 子組件在setupComponent先將這些withCtx包含的渲染函數(shù)存儲在子組件實例對象的slots上;
  3. 子組件渲染的時候,插槽內容的渲染是先找到slots中對應的withCtx包含的渲染函數(shù),然后傳入子組件的pro和默認的渲染DOM內容,最后生成插槽渲染內容的DOM內容。
slot

一句話總結:父組件先編寫DOM存在子組件實例對象上,渲染子組件的時候再渲染對應的這部分DOM內容。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容