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

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

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

使用方法介绍

基本使用

  

父组件传入的内容

我们想将一些内容渲染在Son子组件中,我们在组件中间写了一些内容,例如

父组件传入的内容

,但是最终这些内容会被Vue抛弃,是不会被渲染出来的。

如果我们想将

父组件传入的内容

这部分内容在子组件中渲染,则需要使用slot了。


我们只需要在Son组件模板中加入标签,则

父组件传入的内容

将替换渲染

渲染的结果:

父组件传入的内容

默认内容

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


子组件的默认内容
  • 如果父组件不传入插槽内容,则渲染为:
子组件的默认内容
  • 如果父组件传入插槽内容

    父组件传入的内容

    ,则渲染为:

父组件传入的内容

具名插槽

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


子组件的默认内容

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

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


  

父组件的内容1

父组件的内容2

渲染内容如下:

外部传入的header

父组件的内容1

父组件的内容2

外部传入的footer

v-slot:header包含的内容替换;
v-slot:footer包含的内容替换;
其他所有内容都被当成v-slot:default替换;

插槽作用域

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

  • 父组件


  

插槽的name {{ name }}

setup() { return { name: ref("parent"), } },
  • 子组件

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

渲染结果:

插槽的name: parent
作用域插槽

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

  • 将数据以pro的形式传递

  • 父组件接收pro

此时渲染的内容:

插槽的name: child

实现原理介绍

分析案例:



  

插槽的name {{ name }}


子组件的默认内容
渲染函数分析
  • 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内容。

Vue3.0的插槽是如何实现的?_第1张图片

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

你可能感兴趣的:(Vue,vue.js,前端,javascript)