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

实现原理介绍 分析案例:
【Vue|Vue3.0的插槽是如何实现的()】插槽的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 } = _Vueconst _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 } = _Vuereturn (_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 = https://www.it610.com/article/{}, // 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 } = n2if (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 nameslot的name * @param props slot的pro * @param fallback 默认的内容 * @param noSlotted * @returns */ export function renderSlot( slots: Slots, name: string, props: Data = https://www.it610.com/article/{}, 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内容。
Vue|Vue3.0的插槽是如何实现的()
文章图片

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

    推荐阅读