slot 插槽
slot 插槽
普通插槽
编译发生在调用 vm.$mount
的时候,编译的顺序是先编译父组件再编译子组件。
在 parse
解析 template
模板字符串转换成 AST 树阶段,处理一元标签(例如:<img>
、<br/>
)和闭合标签时,会调用 closeElement(element)
方法,执行processElement(element, options)
方法,调用 processSlotContent(element)
和 processSlotOutlet(element)
方法处理 slot
。
【processSlotContent】方法
// src\compiler\parser\index.ts
// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent(el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (__DEV__ && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (__DEV__ && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
}
// slot="xxx"
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
el.slotTargetDynamic = !!(
el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
)
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
// 2.6 v-slot syntax
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (__DEV__) {
if (el.slotTarget || el.slotScope) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
if (el.parent && !maybeComponent(el.parent)) {
warn(
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
el
)
}
}
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (__DEV__) {
if (!maybeComponent(el)) {
warn(
`v-slot can only be used on components or <template>.`,
slotBinding
)
}
if (el.slotScope || el.slotTarget) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
if (el.scopedSlots) {
warn(
`To avoid scope ambiguity, the default slot should also use ` +
`<template> syntax when there are other named slots.`,
slotBinding
)
}
}
// add the component's children to its default slot
const slots = el.scopedSlots || (el.scopedSlots = {})
const { name, dynamic } = getSlotName(slotBinding)
const slotContainer = (slots[name] = createASTElement(
'template',
[],
el
))
slotContainer.slotTarget = name
slotContainer.slotTargetDynamic = dynamic
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotScope) {
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}
}
}
【processSlotOutlet】方法
// src\compiler\parser\index.ts
// handle <slot/> outlets
function processSlotOutlet(el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (__DEV__ && el.key) {
warn(
`\`key\` does not work on <slot> because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`,
getRawBindingAttr(el, 'key')
)
}
}
}
【示例】使用默认插槽
let AppLayout = {
template:
'<div class="container">' +
'<header><slot name="header"></slot></header>' +
'<main><slot>默认内容</slot></main>' +
'<footer><slot name="footer"></slot></footer>' +
'</div>',
}
let vm = new Vue({
el: '#app',
template:
'<div>' +
'<app-layout>' +
'<h1 slot="header">{{title}}</h1>' +
'<p>{{msg}}</p>' +
'<p slot="footer">{{desc}}</p>' +
'</app-layout>' +
'</div>',
data() {
return {
title: '我是标题',
msg: '我是内容',
desc: '其它信息',
}
},
components: {
AppLayout,
},
})
在父组件中,当解析到标签上有
slot
属性时,会给对于的 AST 元素节点添加slotTarget
属性。在generate
生成可执行代码阶段,在genData(el, state)
方法中会处理slotTarget
,会给data
添加一个slot
属性,并指向slotTarget
。对于示例,父组件最终生成的代码如下:
with (this) { return _c( 'div', [ _c('app-layout', [ _c('h1', { attrs: { slot: 'header' }, slot: 'header' }, [ _v(_s(title)), ]), _c('p', [_v(_s(msg))]), _c('p', { attrs: { slot: 'footer' }, slot: 'footer' }, [ _v(_s(desc)), ]), ]), ], 1 ) }
在编译子组件的时候,同样在
parse
解析template
模板字符串转换成 AST 树阶段,会执行processSlotContent(element)
和processSlotOutlet(element)
。当解析子组件遇到
slot
标签时,会给对应的 AST 元素节点添加slotName
属性在
generate
生成可执行代码阶段,会判断如果当前 AST 元素节点是slot
标签,则执行genSlot(el, state)
函数。可以通过插槽的名称(el.slotName
)拿到对应的scopedSlotFn
,然后把相关的数据扩展到props
上,作为函数的参数传入,然后返回生成的 vnodes,为后续渲染节点用。【genSlot】方法
// src\compiler\codegen\index.ts function genSlot(el: ASTElement, state: CodegenState): string { const slotName = el.slotName || '"default"' const children = genChildren(el, state) let res = `_t(${slotName}${ children ? `,function(){return ${children}}` : '' }` const attrs = el.attrs || el.dynamicAttrs ? genProps( (el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({ // slot props are camelized name: camelize(attr.name), value: attr.value, dynamic: attr.dynamic, })) ) : null const bind = el.attrsMap['v-bind'] if ((attrs || bind) && !children) { res += `,null` } if (attrs) { res += `,${attrs}` } if (bind) { res += `${attrs ? '' : ',null'},${bind}` } return res + ')' }
对于示例,子组件最终生成的代码如下:
with (this) { return _c( 'div', { staticClass: 'container', }, [ _c('header', [_t('header')], 2), _c('main', [_t('default', [_v('默认内容')])], 2), _c('footer', [_t('footer')], 2), ] ) }
_t
函数对应的是renderSlot(name, fallbackRender, props, bindObject)
方法,其中,name
表示插槽名称slotName
,fallbackRender
表示插槽的默认内容生成的vnode
数组。【renderSlot】方法
/** * Runtime helper for rendering <slot> */ export function renderSlot( name: string, fallbackRender: ((() => Array<VNode>) | Array<VNode>) | null, props: Record<string, any> | null, bindObject: object | null ): Array<VNode> | null { const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { // scoped slot props = props || {} if (bindObject) { if (__DEV__ && !isObject(bindObject)) { warn('slot v-bind without argument expects an Object', this) } props = extend(extend({}, bindObject), props) } nodes = scopedSlotFn(props) || (isFunction(fallbackRender) ? fallbackRender() : fallbackRender) } else { nodes = this.$slots[name] || (isFunction(fallbackRender) ? fallbackRender() : fallbackRender) } const target = props && props.slot if (target) { return this.$createElement('template', { slot: target }, nodes) } else { return nodes } }
在
renderSlot
方法中,对于默认插槽,如果this.$slot[name]
有值,就返回它对应的vnode
数组(该数组里的vnode
都是在父组件中创建的,这样既可以实现在父组件替换子组件插槽的内容),否则返回fallbackRender
。其中this.$slot
来源于子组件初始化过程中,执行initRender
函数,获取到vm.$slot
。对于
vm.$slot
,是在initRender
函数中,执行vm.$slots = resolveSlots(options._renderChildren, renderContext)
获得的。在resolveSlots(children, context)
函数中:首先,遍历
children
,获取到每一个child
的data
然后,通过
data.slot
获取到插槽名称。(slot
是在编译父组件在generate
生成可执行代码阶段设置的data.slot
)data.slot
存在,则以插槽名称为key
把对应的child
添加到slots
中。data.slot
不存在,则是默认插槽的内容,则把对应的child
添加到slots.defaults
中。
获取到整个
slots
,它是一个对象,key
是插槽名称,value
是一个vnode
类型的数组,因为它可以有多个同名插槽。
【resolveSlots】方法
export function resolveSlots( children: Array<VNode> | null | undefined, context: Component | null ): { [key: string]: Array<VNode> } { if (!children || !children.length) { return {} } const slots: Record<string, any> = {} for (let i = 0, l = children.length; i < l; i++) { const child = children[i] const data = child.data // remove slot attribute if the node is resolved as a Vue slot node if (data && data.attrs && data.attrs.slot) { delete data.attrs.slot } // named slots should only be respected if the vnode was rendered in the // same context. if ( (child.context === context || child.fnContext === context) && data && data.slot != null ) { const name = data.slot const slot = slots[name] || (slots[name] = []) if (child.tag === 'template') { slot.push.apply(slot, child.children || []) } else { slot.push(child) } } else { ;(slots.default || (slots.default = [])).push(child) } } // ignore slots that contains only whitespace for (const name in slots) { if (slots[name].every(isWhitespace)) { delete slots[name] } } return slots } function isWhitespace(node: VNode): boolean { return (node.isComment && !node.asyncFactory) || node.text === ' ' }
作用域插槽(slot-scope)
【示例】作用域插槽
let Child = {
template:
'<div class="child">' + '<slot text="Hello " :msg="msg"></slot>' + '</div>',
data() {
return {
msg: 'Vue',
}
},
}
let vm = new Vue({
el: '#app',
template:
'<div>' +
'<child>' +
'<template slot-scope="props">' +
'<p>Hello from parent</p>' +
'<p>{{ props.text + props.msg}}</p>' +
'</template>' +
'</child>' +
'</div>',
components: {
Child,
},
})
父组件,在
parse
解析template
模板字符串转换成 AST 树阶段,处理一元标签(例如:<img>
、<br/>
)和闭合标签时,会调用closeElement(element)
方法。对于作用域插槽(slot-scope
):- 会执行
processElement(element, options)
方法,调用processSlotContent(element)
和processSlotOutlet(element)
方法处理slot-scope
。会读取slot-scope
属性并赋值给当前 AST 元素节点的slotScope
属性。 - 在构造 AST 数的时候,对
scopedSlot
属性的 AST 元素节点而言,是不会作为children
添加到当前 AST 树中,而是存到父 AST 元素节点的scopedSlots
属性上,它是一个对象,以插槽名称name
为key
。
- 会执行
父组件,在
generate
生成可执行代码阶段,在调用genData(el, state)
方法根据 AST 元素节点的属性构造出一个data
对象字符串时,会执行if (el.scopedSlots) { data += `${genScopedSlots(el.scopedSlots, state)},` }
对scopedSlots
做处理。在
genScopedSlots
方法中,会对scopedSlots
对象遍历,执行genScopedSlot
方法,并把结果用逗号拼接。在
genScopedSlot
方法中,先生成一段函数代码,并且函数的参数就是的slotScope
,也就是写在标签属性上的slot-scope
对应的值,然后再返回一个对象,key
为插槽名称,fn
为生成的函数代码。【genScopedSlots】方法:处理 scopedSlots
// src\compiler\codegen\index.ts function genScopedSlots( el: ASTElement, slots: { [key: string]: ASTElement }, state: CodegenState ): string { // by default scoped slots are considered "stable", this allows child // components with only scoped slots to skip forced updates from parent. // but in some cases we have to bail-out of this optimization // for example if the slot contains dynamic names, has v-if or v-for on them... let needsForceUpdate = el.for || Object.keys(slots).some(key => { const slot = slots[key] return ( slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic ) }) // #9534: if a component with scoped slots is inside a conditional branch, // it's possible for the same component to be reused but with different // compiled slot content. To avoid that, we generate a unique key based on // the generated code of all the slot contents. let needsKey = !!el.if // OR when it is inside another scoped slot or v-for (the reactivity may be // disconnected due to the intermediate scope variable) // #9438, #9506 // TODO: this can be further optimized by properly analyzing in-scope bindings // and skip force updating ones that do not actually use scope variables. if (!needsForceUpdate) { let parent = el.parent while (parent) { if ( (parent.slotScope && parent.slotScope !== emptySlotScopeToken) || parent.for ) { needsForceUpdate = true break } if (parent.if) { needsKey = true } parent = parent.parent } } const generatedSlots = Object.keys(slots) .map(key => genScopedSlot(slots[key], state)) .join(',') return `scopedSlots:_u([${generatedSlots}]${ needsForceUpdate ? `,null,true` : `` }${ !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : `` })` } function genScopedSlot(el: ASTElement, state: CodegenState): string { const isLegacySyntax = el.attrsMap['slot-scope'] if (el.if && !el.ifProcessed && !isLegacySyntax) { return genIf(el, state, genScopedSlot, `null`) } if (el.for && !el.forProcessed) { return genFor(el, state, genScopedSlot) } const slotScope = el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope) const fn = `function(${slotScope}){` + `return ${ el.tag === 'template' ? el.if && isLegacySyntax ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined` : genChildren(el, state) || 'undefined' : genElement(el, state) }}` // reverse proxy v-slot without scope on this.$slots const reverseProxy = slotScope ? `` : `,proxy:true` return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}` }
对于示例,父组件最终生成的代码如下:
with (this) { return _c( 'div', [ _c('child', { scopedSlots: _u([ { key: 'default', fn: function (props) { return [ _c('p', [_v('Hello from parent')]), _c('p', [_v(_s(props.text + props.msg))]), ] }, }, ]), }), ], 1 ) }
在父组件通过编译后生成的代码中,
_u
函数对应的是resolveScopedSlots(fns, res)
方法,其中,fns
是一个数组,每一个数组元素都有一个key
和一个fn
,key
对应的是插槽的名称,fn
对应一个函数。resolveScopedSlots(fns, res)
方法,通过遍历fns
数组,生成一个对象,对象的key
就是插槽名称,value
就是函数。【resolveScopedSlots】方法
export function resolveScopedSlots( fns: ScopedSlotsData, res?: Record<string, any>, // the following are added in 2.6 hasDynamicKeys?: boolean, contentHashKey?: number ): { $stable: boolean } & { [key: string]: Function } { res = res || { $stable: !hasDynamicKeys } for (let i = 0; i < fns.length; i++) { const slot = fns[i] if (isArray(slot)) { resolveScopedSlots(slot, res, hasDynamicKeys) } else if (slot) { // marker for reverse proxying v-slot without scope on this.$slots // @ts-expect-error if (slot.proxy) { // @ts-expect-error slot.fn.proxy = true } res[slot.key] = slot.fn } } if (contentHashKey) { ;(res as any).$key = contentHashKey } return res as any }
子组件,在编译的时候,同样在
parse
解析template
模板字符串转换成 AST 树阶段,会执行processSlotContent(element)
和processSlotOutlet(element)
。- 当解析子组件遇到
slot
标签时,会给对应的 AST 元素节点添加slotName
属性 - 在
generate
生成可执行代码阶段,会判断如果当前 AST 元素节点是slot
标签,则执行genSlot(el, state)
函数。可以通过插槽的名称(el.slotName
)拿到对应的scopedSlotFn
,然后把相关的数据扩展到props
上,作为函数的参数传入,然后返回生成的 vnodes,为后续渲染节点用。
对于示例,子组件最终生成的代码如下:
with (this) { return _c( 'div', { staticClass: 'child' }, [_t('default', null, { text: 'Hello ', msg: msg })], 2 ) }
_t
函数对应的是renderSlot(name, fallbackRender, props, bindObject)
方法,其中,name
表示插槽名称slotName
,fallbackRender
表示插槽的默认内容生成的vnode
数组。在
renderSlot
方法中,对于this.$scopedSlots[name]
,是在子组件的渲染函数执行前,在vm._render
方法内定义的。- 当解析子组件遇到