vue3-slot多组件作用域传值

414 阅读4分钟

问题

image.png

现在有组件 A,B,C,D 。 A嵌套B , B嵌套C,C嵌套D。 D定义了一个myslot插槽,并且传入参数msg 希望在A组件直接通过具名myslot插入D的内容。并且可以拿到msg的信息。

方法1 组件template转发

通过转发的方式,在B,和C ,都定义相同的命名插槽,并且的作用域同步传入。

image.png

A组件

<template>
<B>
   <template #mysolt="{msg}">
  {{msg}}
   </template>
</B>
</template>

B组件

<template>
<C> 
  <template #mysolt="{msg}">
       <slot name="mysolt" :msg="msg">
        </slot>
 </template>
</C>
</template>

C组件

<template>
<D> 
  <template #mysolt="{msg}">
       <slot name="mysolt" :msg="msg">
        </slot>
 </template>
</D>
</template>

D组件

<template>
  <div>
    <slot name="mysolt" msg="我是D组件msg"> </slot>
  </div>
</template>

缺点需要重复定义,改动大

方法2 CreateVNode + ctx.slots 传递

如果组件是通过CreateVNode创建,可以直接通过获取当前的slots信息 ,并且直接传给下一个CreateVNode里面的slots

image.png

组件A , 定义了插槽

<template>
  <B>
   <template #mysolt="{msg}">
  {{msg}}
   </template >
</B>
</template>

// 等价于 下面 defineComponent定义的写法
<script lang='ts'> 
import B from './B.vue';  
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({
  setup(props, ctx) { 
    return () =>  createVNode(B, props,{ 'myslot' : (msg:string) => {return createVNode('p',null,msg) }} )
  }, 
}) 
</script>

组件B , 引用了A定义的插槽

<script lang='ts'> 
import C from './C.vue';  
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({
  setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
     //等价于    ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }} 
    return () =>  createVNode(C, props,ctx.slots)
  }, 
}) 
</script>

组件C, 还是引用了A定义的插槽

<script lang='ts'> 
import D from './D.vue';  
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({ //ctx.slots 包含了还是B的插槽信息,用的是上面Aslots
  setup(props, ctx) { 
    return () =>  createVNode(D, props,ctx.slots)
  }, 
}) 
</script>

组件D ,最后D插槽直接消费 A组件定义的插槽

<template>
  <div>
    <slot name="mysolt" msg="我是D组件msg">
     </slot>
  </div>
</template>
//等价于下面的defineComponent 的写法

<script lang='ts'>
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({ //ctx.slots 包含了还是B的插槽信息,用的是上面Aslots
  setup(props, ctx) { 
    let msg = '我是D组件的msg'
    return () =>  createVNode('div', props,ctx.slots.myslot('msg'))
  }, 
}) 
</script>

源码分析1

我们写一个 A 直接调用D的 demo

<script src="../../dist/vue.global.js"></script> 
<script type="text/x-template" id="d1"> 
    <h1>D组件</h1>
    <div>
      <slot name="mysolt" msg="我是D组件msg"> </slot>
    </div> 
</script> 
<script>
const D = {
  template: '#d1', 
}
</script> 

<script type="text/x-template" id="a1"> 
    <h1>A组件</h1>
    <D>
     <template #mysolt="{msg}">
    {{msg}}
     </template >
  </D> 
</script> 
<script> 
const A1 = {
  template: '#a1',
  components: {
    D
  }, 
  setup(props) { 
  }
}
</script>


<div id="demo">
  <A1>
  </A1>
</div>

<script>
  debugger
const app = Vue.createApp({
  components: {
    A1
  },
  setup() { 
  }
}) 
app.mount('#demo') 
</script>

<style> 
</style>

image.png

initSlots

image.png

在创建组件A的时候会调用 initSlots 方法,把插槽函数存储到当前实例的slots上面。

export const initSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren,
) => {
  const slots = (instance.slots = createInternalObject()) // 在当前实例创建一个slots对象。里面记录所有插槽信息, 
  if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    const type = (children as RawSlots)._
    if (type) {
      extend(slots, children as InternalSlots) // 这里开始记录
      // make compiler marker non-enumerable
      def(slots, '_', type, true)
    } else {
      normalizeObjectSlots(children as RawSlots, slots, instance)
    }
  } else if (children) {
    normalizeVNodeSlots(instance, children)
  }
}

renderSlot

image.png

通过调用栈,可以看到在创建A里面的D组件时候,使用 _renderSlot($slots, "mysolt", { msg: "我是D组件msg" }) 创建 D组件,其中 $slots是上面保存的组件实例里面的一个map, 里面包含了 A里面定义的插槽信息是一个函数,mysolt = () => {}

image.png

packages/runtime-core/src/helpers/renderSlot.ts

import type { Data } from '../component'
import type { RawSlots, Slots } from '../componentSlots'
import {
  type ContextualRenderFn,
  currentRenderingInstance,
} from '../componentRenderContext'
import {
  Comment,
  Fragment,
  type VNode,
  type VNodeArrayChildren,
  createBlock,
  isVNode,
  openBlock,
} from '../vnode'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { warn } from '../warning'
import { createVNode } from '@vue/runtime-core'
import { isAsyncWrapper } from '../apiAsyncComponent'

/**
 * Compiler runtime helper for rendering `<slot/>`
 * @private
 */
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 {
  if (
    currentRenderingInstance!.isCE ||
    (currentRenderingInstance!.parent &&
      isAsyncWrapper(currentRenderingInstance!.parent) &&
      currentRenderingInstance!.parent.isCE)
  ) {
    if (name !== 'default') props.name = name
    return createVNode('slot', props, fallback && fallback())
  }

  let slot = slots[name]  // 这里 slots 缓存所有插槽的map 

  // a compiled slot disables block tracking by default to avoid manual
  // invocation interfering with template-based block tracking, but in
  // `renderSlot` we can be sure that it's template-based so we can force
  // enable it.
  if (slot && (slot as ContextualRenderFn)._c) {
    ;(slot as ContextualRenderFn)._d = false
  }
  openBlock()
  const validSlotContent = slot && ensureValidVNode(slot(props)) // 这里调用插槽方法
  const rendered = createBlock(
    Fragment,
    {
      key:
        props.key ||
        // slot content array of a dynamic conditional slot may have a branch
        // key attached in the `createSlots` helper, respect that
        (validSlotContent && (validSlotContent as any).key) ||
        `_${name}`,
    },
    validSlotContent || (fallback ? fallback() : []),
    validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
      ? PatchFlags.STABLE_FRAGMENT
      : PatchFlags.BAIL,
  )
  if (!noSlotted && rendered.scopeId) {
    rendered.slotScopeIds = [rendered.scopeId + '-s']
  }
  if (slot && (slot as ContextualRenderFn)._c) {
    ;(slot as ContextualRenderFn)._d = true
  }
  return rendered // 返回一个渲染函数
}

function ensureValidVNode(vnodes: VNodeArrayChildren) {
  return vnodes.some(child => {
    if (!isVNode(child)) return true
    if (child.type === Comment) return false
    if (
      child.type === Fragment &&
      !ensureValidVNode(child.children as VNodeArrayChildren)
    )
      return false
    return true
  })
    ? vnodes
    : null
}

  • renderSlot方法通过前面保存的 slots 找到 对应的插槽方法
  • 使用 slot && ensureValidVNode(slot(props)) 调用该方法,并且返回内容
  • 通过createBlock(Fragment,{...},validSlotContent) 创建新的vnode 实现内容的输出

image.png

源码分析2

多个组件传递

<script src="../../dist/vue.global.js"></script>

<script type="text/x-template" id="d1"> 
    <h1>D组件</h1>
    <div>
      <slot name="mysolt" msg="我是D组件msg"> </slot>
    </div> 
</script> 
<script>
const D = {
  template: '#d1', 
}
</script>

<script type="text/x-template" id="c1"> 
    <h1>C组件</h1>
    <D> 
      <template #mysolt="{msg}">
           <slot name="mysolt" :msg="msg">
            </slot>
     </template >
    </D> 
</script> 
<script>
const C = {
  template: '#c1',
  components: {
    D
  }
}
</script>


<script type="text/x-template" id="b1"> 
    <h1>B组件</h1>
    <C> 
      <template #mysolt="{msg}">
           <slot name="mysolt" :msg="msg">
            </slot>
     </template >
    </C> 
</script> 
<script>
const B1 = {
  template: '#b1',
  components: {
    C
  }
}
</script>

<script type="text/x-template" id="a1"> 
    <h1>A组件</h1>
    <B1>
     <template #mysolt="{msg}">
    {{msg}}
     </template >
  </B1> 
</script> 
<script>
const { reactive, computed,ref,h } = Vue 
const A1 = {
  template: '#a1',
  components: {
    B1
  }, 
  setup(props) { 
  }
}
</script>


<div id="demo">
  <A1>
  </A1>
</div>

<script>
  debugger
const app = Vue.createApp({
  components: {
    A1
  },
  setup() { 
  }
}) 
app.mount('#demo') 
</script>

<style> 
</style>


image.png

可以看到renderSlot 触发时机是在 D组件需要渲染时候,依此从D往B 执行renderSlot

  • D组件 执行renderSlot
  • C组件 执行renderSlot
  • B组件 执行renderSlot

第一次 D组件 image.png

第二次 C组件 image.png

第三次 B组件 image.png

源码分析3

<script src="../../dist/vue.global.js"></script>

<div id="demo">
  <A1>
  </A1>
</div>
 
<script>
  const { defineComponent,h ,createVNode} = Vue

// 定义 D 组件
const D = defineComponent({
  template: `
    <h1>D组件</h1>
    <div>
      <slot name="mysolt" msg="我是D组件msg"></slot>
    </div>
  `
});

// 定义 C 组件
const C = defineComponent({
  components: { D },
  setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
     //等价于    ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }} 
    return () =>  createVNode(D, props,ctx.slots)
  }, 
});

// 定义 B1 组件
const B1 = defineComponent({
  components: { C },
  setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
     //等价于    ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }} 
    return () =>  createVNode(C, props,ctx.slots)
  }, 
});

// 定义 A1 组件
const A1 = defineComponent({
  components: { B1 },
  template: `
    <h1>A组件</h1>
    <B1>
      <template #mysolt="{ msg }">
        {{ msg }}
      </template>
    </B1>
  `
});

debugger
// 创建 Vue 应用并挂载 
const { createApp } = Vue
const app = createApp({
  components: { A1 }
});

app.mount('#demo');

</script>

image.png

可以看到中间的B 和 C组件使用 defineComponent定义 ctx.slots 透传的效果和 源码2 效果一致

源码参考

github.com/mjsong07/vu…

github.com/mjsong07/vu…