time: 2021-05-06 16:26:58
author: heyunjiang
<slot name="header">
+ <template v-slot:header>
,任何没有被 template + v-slot 包裹的内容,都会被视为 v-slot:default
内容<slot name="header" :user="user">
+ <template v-slot:header="slotProps">
,从而实现父组件插槽读取子组件数据<template #header="slotProps">
// index.vue
<div id="app">
<HelloWorld>
hello
<template v-slot:footer>footer</template>
world
</HelloWorld>
</div>
// helloworld.vue
<div class="hello">
<slot></slot>
<slot name="footer"></slot>
</div>
// 编译结果
// index.vue
_c('div',{attrs:{"id":"app"}},[_c('HelloWorld',{scopedSlots:_vm._u([{key:"footer",fn:function(){return [_vm._v("footer")]},proxy:true}])},[_vm._v(" hello "),_vm._v(" world ")])],1)
// helloworld.vue
_c('div',{staticClass:"hello"},[_vm._t("default"),_vm._t("footer")],2)
结果分析
来看看 _v, _t, _u 是啥
在 renderMixin 中,installRenderHelpers(Vue.prototype)
,加载了下列方法
import { createTextVNode } from 'core/vdom/vnode'
import { renderSlot } from './render-slot'
import { resolveScopedSlots } from './resolve-scoped-slots'
export function installRenderHelpers (target: any) {
target._t = renderSlot
target._v = createTextVNode
target._u = resolveScopedSlots
}
归纳如下
操作父组件的 slot 内容
// template
<div id="app">
<HelloWorld>
hello
<template v-slot:footer="slotProps">footer</template>
</HelloWorld>
</div>
// 编译结果
_c('div',{attrs:{"id":"app"}},[_c('HelloWorld',{scopedSlots:_vm._u([{key:"footer",fn:function(slotProps){return [_vm._v("footer"+_vm._s(slotProps.number))]}}])},[_vm._v(" hello ")])],1)
编译结果分析
来看看 resolveScopedSlots 源码,看看它的返回结果是啥
export function resolveScopedSlots (
fns: ScopedSlotsData,
res?: Object,
hasDynamicKeys?: boolean
): { [key: string]: Function, $stable: boolean } {
res = res || { $stable: !hasDynamicKeys }
for (let i = 0; i < fns.length; i++) {
const slot = fns[i]
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys)
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
slot.fn.proxy = true
}
res[slot.key] = slot.fn
}
}
return res
}
归纳分析
编译成函数
问题:
操作的是子组件的 slot 节点
// template
<div class="hello">
<slot></slot>
<slot name="footer" :number="2"></slot>
</div>
// 编译结果
_c('div',{staticClass:"hello"},[_vm._t("default"),_vm._t("footer",null,{"number":2})],2)
从编译结果来看,要求 renderSlot 执行结果为子节点 vnode renderSlot 源码
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) {
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
}
}
源码结果
vm.$scopedSlots[name]
属性,也就是说父组件的 scopedSlot 内容已经编译到一个对象里面,结果是一个函数 objectvm.$scopedSlots[name]
获取对应 fn,并且把子组件内的作用域插槽参数传入执行 fn,返回对应的 vnode 节点,作为一个当前 render vnode tree 上的一个节点到目前为止,我们已经知道具名插槽的渲染流程
函数
对象,作为父组件 vnode.data.scopedSlots 属性传入,在父组件 vm._render 时生成 vm.$scopedSlots问题:vm.$slots 是啥?和 children 有什么关系
答:vm.$slots 是静态插槽,vm.$scopedSlots 是作用域插槽;$slots 是由 children 生成的
再次回顾 components 渲染流程,看看组件的 children 是如何处理的
chidren
会作为 vnode.componentOptions.chidlren 存在
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
) {
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
函数式组件需要主动调用 context.children 来渲染
// 1 生成的组件 vnode 会作为 new vue 的 options._parentVnode 传入
export function createComponentInstanceForVnode (
vnode: any,
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
return new vnode.componentOptions.Ctor(options)
}
// 2 initRender 将之前的 children 转成 vm.$slots
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext) // _renderChildren 即 component vnode.componentOptions.children,renderContext 即组件自身 vnode.context
vm.$scopedSlots = emptyObject
}
// 3 在组件调用自身 _render 生成 vnode tree 时,生成 vm.$scopedSlots 对象
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
)
}
}
归纳分析
vm.$slots
,将 child vnode push 到 vm.$slots.default 中vm.$scopedSlots
之后,在后续的 render.call 生成 vnode 时,slot 会调用 renderSlot 读取 vm.$scopedSlots 上的 slot 函数resolveSlots 源码
export function resolveSlots (
children: ?Array<VNode>,
context: ?Component
): { [key: string]: Array<VNode> } {
if (!children || !children.length) {
return {}
}
const slots = {}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
(slots.default || (slots.default = [])).push(child)
}
return slots
}
函数
,default slot 被编译成 _c 数组
_render
时,会通过 normalizeScopedSlots 生成 vm.$scopedSlots 属性对象initRender
时,会调用 resolveSlots 读取 生成 vm.$slots 属性对象关键点归纳:scopedSlot 会被编译成函数供父组件调用,类似于高阶函数实现