一、从一个例子开始
var CompB = {
props: {
message: {
type:String
}
},
template: `
<div>
B {{message}}
<slot />
</div>
`
}
var CompC = {
data() {
return {
msg: "我是组件B"
}
},
methods: {
handleClick1() {
this.msg = "2"
}
},
template: `
<div>
C
<slot />
<slot name="header"></slot>
<slot name="footer" v-bind:message="msg"></slot>
</div>
`
}
new Vue({
el: '#app',
components: {
CompB,
CompC,
},
data: {
content: '我是root组件的数据'
},
template: `
<CompC>
<div slot="header">header slot</div>
<div>default slot</div>
<template slot="footer" slot-scope="scope">
<CompB :message="scope.message">
<div>{{content}}</div>
</CompB>
</template>
</CompC>
`
})
二、准备工作-编译后的render方法
-
root组件的render方法
function anonymous() { with (this) { return _c( 'CompC', { scopedSlots: _u([ { key: 'footer', fn: function(scope) { return [ _c('CompB', { attrs: { message: scope.message } }, [ _c('div', [_v(_s(content))]), ]), ]; }, }, ]), }, [ _c('div', { attrs: { slot: 'header' }, slot: 'header' }, [_v('header slot')]), _v(' '), _c('div', [_v('default slot')]), ] ); } }- 普通slot是放在
children数组里的,会直接生成一个vnode对象。 - 因为scopedSlot比较特殊,所以是放在
data里的,会生成一个function。
- 普通slot是放在
-
CompC组件的render方法
function anonymous() {
with (this) {
return _c(
'div',
[
_v('\n C\n '),
_t('default'),
_v(' '),
_t('header'),
_v(' '),
_t('footer', null, { message: msg }),
],
2
);
}
}
- CompB组件的render方法
function anonymous() {
with (this) {
return _c('div', [_v('\n B ' + _s(message) + '\n '), _t('default')], 2);
}
}
三、需要了解的东西
1. render辅助方法
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList // 渲染列表
target._t = renderSlot // 渲染slot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots // 渲染scopedSlot
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
2. render辅助方法之resolveScopedSlots
resolveScopedSlots的代码:
export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
res?: Object,
// the following are added in 2.6
hasDynamicKeys?: boolean,
contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {
// $stable为true时则表示稳定的
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
}
}
if (contentHashKey) {
(res: any).$key = contentHashKey
}
return res
}
- 执行
_u(也就是resolveScopedSlots)会遍历定义的所有scopedSlot, 这里我们以执行root组件的render方法为例,调用_u方法传入的是一个数组。数组中的每一项都是一个对象,对象有两个属性key和fn。key就是slot的名字。fn就是我们定义的具体的内容,fn的参数是从c组件里传进来的数据,fn的返回值是vnode对象。- 为什么fn是一个函数呢?这跟scopedSlot的设计有关,scopedSlot既能使用父组件(root组件)的数据,也能使用子组件(CompC组件)传递的数据(也就是fn的参数),所以得设计成一个函数。
在了解了上面的东西之后,我们开始走一遍整个渲染过程,相信你应该就能明白slot的整个渲染流程了。为了减少干扰和负担,我们只关心与slot有关的细节, 所以在渲染过程中与slot无关的流程细节就不再描述了。
四、root组件执行render方法得到vnode
-
CompC的scopedSlot的处理- 编译后render方法内部的
scoped是这样的。
{ scopedSlots: _u([ { key: 'footer', fn: function(scope) { return [ _c('CompB', { attrs: { message: scope.message } }, [ _c('div', [_v(_s(content))]), ]), ]; }, }, ]), },- 执行
_u的返回值是一个对象。
{ $stable: true footer: ƒ (scope) }- 返回值将会作为
vnode的scopedSlots属性值。
scopedSlots: { $stable: true footer: ƒ (scope) } - 编译后render方法内部的
-
在自定义组件
vnode.data上安装hook,比如说下面要讲到的init hook
installComponentHooks(data)
- 创建
CompC组件的vnode时传入的componentOptions,请注意componentOptions很重要后面会用到。
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, // componentOptions
asyncFactory
)
- 执行
root组件的render方法后得到的vnode是这样的。
五、root组件进行patch
- 因为是第一次
patch的原因所以会执行createElm,而不是执行patchVnode。
- 在
createElm会分两种情况- 如果vnode是一个
自定义组件节点则调用createComponet创建组件实例。 - 如果是一个普通的
dom元素节点则通过dom api创建即可。
- 如果vnode是一个
在root组件template里的根节点是一个自定义组件CompC,所以调用createComponet的时候会创建一个组件实例。
创建内部组件是通过调用vnode的
init hook创建的,普通的dom vnode是没有init hook的。
六、CompC组件创建实例
-
在
init hook里面调用createComponentInstanceForVnode创建实例。init (vnode: VNodeWithData, hydrating: boolean): ?boolean { // ...... const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, // VNode节点对象 activeInstance // 父组件实例 ) // ...... // 挂载组件 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } -
createComponentInstanceForVnode的代码export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, // 是否是自定义组件 _parentVnode: vnode, // 子组件的$Vnode parent // 父组件的实例vm } // ....... /* componentOptions对象上保存了创建组件对象时的一些信息 */ return new vnode.componentOptions.Ctor(options) }- 创建内部组件实例的时候传入的
option对象是和root组件传入的数据结构不一样的。 vnode.componentOptions.Ctor就是通过Vue.extend(options)返回的。
- 创建内部组件实例的时候传入的
七、CompC组件执行render
1. 组件实例获取vm.$scopedSlots
与root组件执行render有点不同的是, 在Vue.prototype._render内部调用组件的render方法之前会先获取到组件CompC实例上的vm.$scopedSlots。
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
2. normalizeScopedSlots的逻辑
export function normalizeScopedSlots (
slots: { [key: string]: Function } | void,
normalSlots: { [key: string]: Array<VNode> },
prevSlots?: { [key: string]: Function } | void
): any {
let res
// vm.$slots上本来有的数据
const hasNormalSlots = Object.keys(normalSlots).length > 0
// slots是否稳定?
// 1、传入的slots有$stable标识
// 2、vm.$slots不存在
const isStable = slots ? !!slots.$stable : !hasNormalSlots
const key = slots && slots.$key
// 如果slots不存在
if (!slots) {
res = {}
// 如果_normalized == true表示slots已经处理过了,直接返回
} 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
// 将普通的slots代理到$scopedSlots上,是为了和scopedSlot统一
// 在执行renderSlot方法时统一调用slot对应的方法返回vnode对象
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
}
// 对scopedSlot返回的vnode进行normalizeChildren处理
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
}
// 为了统一处理,普通的slot也会使用一个对象
function proxyNormalSlot(slots, key) {
return () => slots[key]
}
3. CompC组件实例的vm.$scopedSlots是这样的
我们在root组件的template内给Compc组件定义的default、header、footer三个slot终于传到了Compc组件实例上。传递的过程是这样的。
- root组件的render方法内创建slot
- scopedSlots保存在
CompC组件对应的vnode.data.scopedSlots上。 - slots保存在
vnode.componentOptions.children上。
- scopedSlots保存在
- Compc组件执行render的方法之前实例从
vnode上获取slot。
4.执行render
CompC组件执行render方法,也就是下面这段代码:
function anonymous() {
with (this) {
return _c(
'div',
[
_v('\n C\n '),
_t('default'),
_v(' '),
_t('header'),
_v(' '),
_t('footer', null, { message: msg }),
],
2
);
}
}
从上面我们就会发现每个<slot>标签都是通过_t创建的。在第三节中我们已经知道target._t = renderSlot。所以我们看看renderSlot的代码都做了什么?
- 跟据
slot的name从$scopedSlots上面拿到对应的slot,如果没有则渲染fallback的内容。fallback的内容也就是定义在<slot>标签内的东西。 - 如果是作用域插槽则会将
CopmC组件内通过<slot>标签传递的属性作为参数。如果是普通插槽的话直接执行即可。
export function renderSlot (
name: string, // slot名称
fallback: ?Array<VNode>, // <slot name="header"></slot> 标签里定义的默认内容
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
// 在前面我们知道$scopedSlots已经在执行render方法之前获取到了
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)
}
// 将绑定的对象传入到scopedSlotFn
nodes = scopedSlotFn(props) || fallback
} else {
// 非作用域插槽
nodes = this.$slots[name] || fallback
}
// <slot name="a" slot="b"></slot>
// slot的传递,即孙子组件传到爷爷组件哪里去了, 父组件只是中转了一下
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
总结
- 在执行
CompC组件的render方法后,slot的任务已经转成了vnode, 任务也就完成了。 slot是在父组件编译的, 但是是在子组件内使用的。
3.CompC组件render返回的vnode
接下来的逻辑就是组件CompC内部进行patch的逻辑了,这里就不属于这篇文章要将的内容了,如果感兴趣的朋友可以关注我,我有时间会更新这部分内容。