插槽实现原理
首先是父组件:
父组件使用
javascript
// 1. 父组件使用时
<ChildComponent class="component">
<template v-slot:default="scope">
{{ scope }}
</template>
</ChildComponent>
父组件parse
javascript
// 2. 经过 parse 编译成 ast 后 (调用的是 processSlotContent 方法)
// 绑定的值(由子组件传递)
el.slotScope = scope
// 绑定的 slot 名称
el.slotTarget = "default"
// 动态绑定的 slot 名称
el.slotTargetDynamic = default
// template 内部 children 代码
el.children = []
// ChildComponent 对应的 ast 下的 scopedSlots 记录着 slot 的 ast
currentParent.scopedSlots = {}))[name] = element
父组件generate
genData
javascript
// 3. 经过 generate 生成代码后(先调用 genData 方法,此时会调用 genScopedSlots 方法)
if (el.slotTarget && !el.slotScope) {
// 如果不存在 slotScope,表示没有传递参数
data += `slot:${el.slotTarget},`
}
// 对于 ChildComponent 这个组件
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
genScopedSlots
javascript
// 4. genScopedSlots 方法
function genScopedSlots(
el: ASTElement,
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
// 通过这里判断 slot 是否需要强制更新。
let needsForceUpdate = el.for || Object.keys(slots).some(key => {
const slot = slots[key]
return (
slot.slotTargetDynamic || // 如果存在动态绑定的类型
slot.if || // 如果在 if 中
slot.for || // 如果在 for 中
containsSlotChild(slot) // 如果内部还存在 slot 标签
)
})
let needsKey = !!el.if
// 继续判断是否需要更新
// 不需要强制更新时,判断是否在 for 循环或 if 当中
// 判断 parent.slotScope 存在
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
}
}
// 键值对形式: [{ key: "default": fn: fn, proxy: false/true }]
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)}` : ``
})`
}
genScopedSlot
javascript
function genScopedSlot(
el: ASTElement,
state: CodegenState
): string {
// 1. 使用到的变量,将其作为参数
const slotScope = el.slotScope === emptySlotScopeToken
? ``
: String(el.slotScope)
// 2. 定义可以生成 vnode 的函数,并传入定义的参数
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)
}}`
const reverseProxy = slotScope ? `` : `,proxy:true`
// 3. 生成键值对形式
return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}
父组件render
javascript
export function resolveScopedSlots(
/**
* 例如
* [
* {
* key: default
* fn: (user) => {...}
* }
* ]
*/
fns: ScopedSlotsData,
res?: Object, // null 或者 undefined 或 {xx}
hasDynamicKeys?: boolean, // 是否强制更新
contentHashKey?: number // 不强制更新时的 hash
): { [key: string]: Function, $stable: boolean } {
// ! $stable 根据 hasDynamicKeys 判断,在 generate 的时候已经生成
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) {
if (slot.proxy) {
slot.fn.proxy = true
}
res[slot.key] = slot.fn
}
}
// 转换成了 res { default: fn1, header: fn2 }
if (contentHashKey) {
(res: any).$key = contentHashKey
}
return res
}
子组件开始实例化
父组件编译完毕后,更新时,会遇到ChildComponent
组件,进入到子组件的创建环节,此时data
里面包含处理后的scopedSlots
。
javascript
export function createComponentInstanceForVnode (
vnode: any,
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, // vnode.data 中存放着 scopedSlots
parent // 指向当前更新的组件的实例
}
// render 阶段 createComponent 生成vnode的时候,
// options 都存在componentOptions中
return new vnode.componentOptions.Ctor(options)
}
子组件合并选项
javascript
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
// * Vue 构造函数的 options
// * const options: InternalComponentOptions = {
// * _isComponent: true,
// * _parentVnode: vnode,
// * parent
// * }
const opts = vm.$options = Object.create(vm.constructor.options)
// _parentVnode 为组件的 vnode
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
// 组件传值
opts.propsData = vnodeComponentOptions.propsData
// 组件监听的事件
opts._parentListeners = vnodeComponentOptions.listeners
// 组件内部的内容解析为 children。
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
}
子组件数据初始化
javascript
export function initRender(vm: Component) {
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
// _renderChildren 为父组件调用子组件时子组件之间的节点。
// 通过 resolveSlots方法 将 children 转换成 slots 形式
// slots = { default: [Vnode1, Vnode2], [name]: [Vnode], ... }
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
}
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]
const data = child.data
// 删除 data.attrs.slot 属性
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// 如果 data.slot 存在,说明此时 slotScope 不存在(genData时处理的)
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 {
// 其余的默认放到 default 中
(slots.default || (slots.default = [])).push(child)
}
}
// 忽略无用 slot
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
子组件slot使用
javascript
<slot name="header"></slot>
子组件slot parse
javascript
function processSlotOutlet (el) {
if (el.tag === 'slot') {
// 添加 slotName 属性
el.slotName = getBindingAttr(el, 'name');
}
}
子组件slot generate
javascript
function genSlot(el: ASTElement, state: CodegenState): string {
const slotName = el.slotName || '"default"'
// 默认的 children 节点
const children = genChildren(el, state)
// _t 函数
let res = `_t(${slotName}${children ? `,${children}` : ''}`
// 静态属性 + 动态绑定属性
const attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
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 + ')'
}
子组件 render
_render
javascript
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
// 如果有插槽有 scope 的时候,scopedSlots存在
_parentVnode.data.scopedSlots,
// initRender 解析出来的 slots,通过 children 转换而来
vm.$slots,
// 当前的 scopedSlots
vm.$scopedSlots
)
}
// ...
}
normalizeScopedSlots
javascript
export function normalizeScopedSlots(
// 有 scope 时解析出来的 scopedSlots
slots: { [key: string]: Function } | void,
// children 转换而来的 slots,都是没有 slotScope 的 slot
normalSlots: { [key: string]: Array<VNode> },
// 当前组件的原来的 scopeSlots
prevSlots?: { [key: string]: Function } | void
): any {
let res
// 是否具有普通的 slots
const hasNormalSlots = Object.keys(normalSlots).length > 0
// $stable 表示是否需要强制更新,不需要则为true
const isStable = slots ? !!slots.$stable : !hasNormalSlots
const key = slots && slots.$key
if (!slots) {
// ...
} else {
res = {}
for (const key in slots) {
if (slots[key] && key[0] !== '$') {
// 用 scope 的覆盖 没有 scope 的
res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
}
}
}
// 将普通的内容也转换成 scopedSlots 形式
for (const key in normalSlots) {
if (!(key in res)) {
// 将 normal 转变为函数形式
res[key] = proxyNormalSlot(normalSlots, key)
}
}
// * 当前的插槽上面设置的属性
def(res, '$stable', isStable)
def(res, '$key', key)
def(res, '$hasNormal', hasNormalSlots)
// res 里包含了
return res
}
renderSlot
javascript
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)
}
// 执行函数,返回 vnodes
nodes = scopedSlotFn(props) || fallback
} else {
// 如果找不到对应的 name,那么使用默认的 vnodes
nodes = this.$slots[name] || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
总结
插槽在处理的时候主要分为两种类型:
- 一种是带有
slotScope
的,此时具有参数传入。 - 一种是不带有
slotScope
,为普通插槽。
在编译的时候:
- 带有
slotScope
的转换为scopedSlots
对象,存放到component
组件的vnode.data
里面:键为slot name
,值为能生成vnode
的函数(函数的参数与scope
值一致)。 - 不带有
slotScope
的标记但是slotTarget
存在,此时会标记el.data.slot
为true
。 - 对于组件下的内容,全部都会存放到
vnode.componentOptions.children
属性中。
在子组件初始化的时候:
- 用
vm.$options._renderChildren
表示children
。 - 将
children
内容中存在data.slot
的child
,转化到vm.$slots
属性当中,为{ default: [Vnode1, Vnodee2] }
形式。
在子组件渲染的时候:
- 将
vm.$slots
与vm.$scopedSlots
合并到vm.$scopedSlots
,转换为{ default: (props) => Vnodes}
的形式。 - 渲染
slot
组件时,根据name
从vm.$scopedSlots
中获取对应的函数,执行返回vnodes
。