高频面试题:
vue
中的v-slot
?
答案:v-shot
产生的主要目的是,在组件的使用过程中可以让父组件有修改子组件内容的能力,就像在子组件里面放了个插槽,让父组件往插槽内塞入父组件中的楔子;并且,父组件在子组件中嵌入的楔子也可以访问子组件中的数据。v-slot
的产生让组件的应用更加灵活。
一、具名插槽
// 在main.js文件中
let baseLayout = {
template: `<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>`,
data() {
return {
url: ""
};
}
};
new Vue({
el: "#app",
template: `<base-layout>
<template v-slot:header>
<h1>title-txt</h1>
</template>
<p>paragraph-1-txt</p>
<p>paragraph-2-txt</p>
<template v-slot:footer>
<p>foot-txt</p>
</template>
</base-layout>`,
components: {
baseLayout
}
});
当前例子渲染成真实的页面需要经历编译,虚拟DOM的获取和渲染过程。
1、编译部分
(1)ast
在ast
的创建过程中,当执行到闭合标签时,会进行闭合标签的属性处理closeElement(element)
:
function closeElement (element) {
trimEndingWhitespace(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
其中的 element = processElement(element, options)
逻辑中会执行到slot
相关的处理逻辑,这里主要看版本号大于2.6+
的情况:
// 2.6 v-slot syntax
if (el.tag === 'template') {
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (process.env.NODE_ENV !== 'production') {
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
}
}
通过const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
获取到包含name
和value
信息的当前的slot
描述对象,并删除el.attrsList
中关于v-slot
的属性信息。再获取到el
上slotTarget
、slotTargetDynamic
和slotScope
的信息。
在closeElement(element)
过程中,如果存在element.slotScope
,还会将当前element
映射到当前父级的currentParent.scopedSlots
中:
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
var name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
在当前例子中,在最后</base-layout>
闭合过程中,会存在带有slotScope
属性的节点:
通过
element.children = element.children.filter(c => !(c: any).slotScope)
进行过滤。
最后执行结果为:
(2)generate
在generate
阶段,会执行到genData
函数拼接code
字符串,针对scopedSlots
会有逻辑:
export function genData (el: ASTElement, state: CodegenState): string {
// ...
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
// ...
}
其中的genScopedSlots
中有主要逻辑:
function genScopedSlots (
el: ASTElement,
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
// ...
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}}`
}
通过Object.keys(slots)
获取到包含header
和footer
的数组,通过遍历执行genScopedSlot
的逻辑并进行join(',')
拼接。genScopedSlot
中获取到包含子节点创建的fn
函数,如果当前el.slotScope
为"_empty_"
,在其后拼接",proxy:true"
。
执行完遍历后会拼接"scopedSlots:_u([
,最终的获得的render
为:
with(this) {
return _c('base-layout', {
scopedSlots: _u([{
key: "header",
fn: function () {
return [_c('h1', [_v("title-txt")])]
},
proxy: true
}, {
key: "footer",
fn: function () {
return [_c('p', [_v("foot-txt")])]
},
proxy: true
}])
}, [_v(" "), _c('p', [_v("paragraph-1-txt")]), _v(" "), _c('p', [_v("paragraph-2-txt")])])
}
2、init
钩子函数
父节点base-layout
在patch
的过程中,会执行到钩子函数init
:
// inline hooks to be invoked on component VNodes during patch
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
// ...
}
(1)resolveSlots
其中在createComponentInstanceForVnode
时会执行子组件的构造函数,并执行从Vue
继承的this._init
初始化方法,其中initRender
中有处理slot
中默认插槽的逻辑:
/**
* Runtime helper for resolving raw children VNodes into a slot object.
*/
function resolveSlots (
children,
context
) {
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
// ...
(slots.default || (slots.default = [])).push(child);
// ...
}
return slots
}
当前例子中将children
中的子节点都push
到slots.default
中,这里就获取到了defalut
部分的vNode
列表,下面再看获取header
和footer
的相关逻辑。
(2)normalizeScopedSlots
在child.$mount(hydrating ? vnode.elm : undefined, hydrating)
的过程中会执行到_render
逻辑中的:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// ...
}
这里通过normalizeScopedSlots
的方式去合并包含header
和footer
的_parentVnode.data.scopedSlots
和默认default
的vm.$slots
:
export function normalizeScopedSlots (
slots: { [key: string]: Function } | void,
normalSlots: { [key: string]: Array<VNode> },
prevSlots?: { [key: string]: Function } | void
): any {
let res
const hasNormalSlots = Object.keys(normalSlots).length > 0
const isStable = slots ? !!slots.$stable : !hasNormalSlots
const key = slots && slots.$key
if (!slots) {
res = {}
} else if (slots._normalized) {
// fast path 1: child component re-render only, parent did not change
return slots._normalized
} else if (
isStable &&
prevSlots &&
prevSlots !== emptyObject &&
key === prevSlots.$key &&
!hasNormalSlots &&
!prevSlots.$hasNormal
) {
// fast path 2: stable scoped slots w/ no normal slots to proxy,
// only need to normalize once
return prevSlots
} else {
res = {}
for (const key in slots) {
if (slots[key] && key[0] !== '$') {
res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
}
}
}
// expose normal slots on scopedSlots
for (const key in normalSlots) {
if (!(key in res)) {
res[key] = proxyNormalSlot(normalSlots, key)
}
}
// avoriaz seems to mock a non-extensible $scopedSlots object
// and when that is passed down this would cause an error
if (slots && Object.isExtensible(slots)) {
(slots: any)._normalized = res
}
def(res, '$stable', isStable)
def(res, '$key', key)
def(res, '$hasNormal', hasNormalSlots)
return res
}
function normalizeScopedSlot(normalSlots, key, fn) {
const normalized = function () {
let res = arguments.length ? fn.apply(null, arguments) : fn({})
res = res && typeof res === 'object' && !Array.isArray(res)
? [res] // single vnode
: normalizeChildren(res)
return res && (
res.length === 0 ||
(res.length === 1 && res[0].isComment) // #9658
) ? undefined
: res
}
// this is a slot using the new v-slot syntax without scope. although it is
// compiled as a scoped slot, render fn users would expect it to be present
// on this.$slots because the usage is semantically a normal slot.
if (fn.proxy) {
Object.defineProperty(normalSlots, key, {
get: normalized,
enumerable: true,
configurable: true
})
}
return normalized
}
通过Object.defineProperty
为normalSlots
定义属性key
的时候,会执行normalized
函数,这里完成fn
的执行,即获取到fn
函数对应的vNode
,例如:footer
对应的ƒ (){return [_c('p',[_v("foot-txt")])]}
会转换成包含内容为foot-txt
文本vNode
的vNode
。经过normalizeScopedSlot
的执行,使得vm.$slots
中除了包含default
的vNode
列表外还包含header
和footer
的列表。
至此,vm.$scopedSlots
也包含了default
、header
和footer
的可获取到vNode
列表的函数。
(3)_render
的执行
子组件的render
函数为:
with(this) {
return _c('div', {
staticClass: "container"
}, [_c('header', [_t("header")], 2), _v(" "), _c('main', [_t("default")], 2), _v(" "), _c(
'footer', [_t("footer")], 2)])
}
当执行到子组件的_render
时,会执行到t(header)
,即renderSlot
:
function renderSlot (
name,
fallback,
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
props = extend(extend({}, bindObject), props);
}
nodes = scopedSlotFn(props) || fallback;
} else {
nodes = this.$slots[name] || fallback;
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
这里通过var scopedSlotFn = this.$scopedSlots[name]
获取到header
获取vNode
的函数,然后执行nodes = scopedSlotFn(props) || fallback
的方式执行header
对应函数的执行获取vNode
。
二、 作用域插槽
let currentUser = {
template: `<span>
<slot name="user" v-bind:userData="childData">{{childData.firstName}}</slot>
</span>`,
data() {
return {
childData: {
firstName: "first",
lastName: "last"
}
};
}
};
new Vue({
el: "#app",
template: `<current-user>
<template v-slot:user="slotProps">{{slotProps.userData.lastName}}</template>
</current-user>`,
components: {
currentUser
}
});
1、编译过程
(1)ast
父组件编译后的ast
为:
(2)generate
在generate
阶段,不同的是在作用域插槽中通过const slotScope = el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope)
获取到了slotScope
的值。最终获得render
结果为:
with(this) {
return _c('current-user', {
scopedSlots: _u([{
key: "user",
fn: function (slotProps) {
return [_v(_s(slotProps.userData.lastName))]
}
}])
})
}
2、init
阶段
当前例子中子组件currentUser
的render
为:
with(this) {
return _c('span', [_t("user", [_v(_s(childData.firstName))], {
"userData": childData
})], 2)
}
其中的_t
为renderSlot
:
/**
* Runtime helper for rendering <slot>
*/
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) { // scoped slot
props = props || {}
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
)
}
props = extend(extend({}, bindObject), props)
}
nodes = scopedSlotFn(props) || fallback
} else {
nodes = this.$slots[name] || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
作用域插槽不同的是在执行renderSlot
中的nodes = scopedSlotFn(props) || fallback
时,会将props
作为参数传入,在当前例子中就是{"userData": childData}
。然后通过const scopedSlotFn = this.$scopedSlots[name]
获取到当前slot
对应的回调函数this.$scopedSlots[name]
,在当前例子中是function (slotProps) { return [_v(_s(slotProps.userData.lastName))] }
。
在当前例子中执行完回调函数后,就实现了<slot name="user" v-bind:userData="childData">{{childData.firstName}}</slot>
中childData
作为回调参数传入,{{childData.firstName}}
被父组件中内容{{slotProps.userData.lastName}}
替换的目的。
总结:
v-slot
实际上是通过回调函数传参的形式进行了父组件内容替换子组件中插槽的内容。
后记
如有纰漏,请贵手留言~
如有帮助,点赞收藏吆~