vue-slot

普通slot

  • 父组件update的时候已经创建了vnode, 子组件render的时候直接从$slot返回对应的vnode

父组件parse 阶段

  • 往ast节点添加属性slot=slottarget, gendata生成对应字符串
  • compile完 render, 创建子组件vnode后, 开始渲染子组件
1
2
3
4
5
6
7
8
9
function processElement (
element,
options
) {
processSlotContent(element);
processSlotOutlet(element);
processAttrs(element);
return element
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent (el) {
var slotScope;
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope');
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
el.slotScope = slotScope;
}
// slot="xxx"
var 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'));
}
}
}

genData 阶段

  • 添加代码"slot:" + (el.slotTarget) + ",到render字符串中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function genData$2 (el, state) {
var data = '{';
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += "slot:" + (el.slotTarget) + ",";
}
}
// parse=> ast, ast-gencode=>
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)}

子组件(compile) parse和gencode阶段

  • 子组件在实例化的_init设置了$slots
    • initRender: vm.$slots = resolveSlots(options._renderChildren, renderContext);
  • parse时slot ast节点添加slotName属性
  • gencode拼上_t
1
2
3
4
5
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// gencode
function genElement (el, state) {
if (el.tag === 'slot') {
return genSlot(el, state)
}
}
function genSlot (el, state) {
var slotName = el.slotName || '"default"';
var children = genChildren(el, state); // slot包裹子节点
var res = "_t(" + slotName + (children ? ("," + children) : '');
var attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(function (attr) { return ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
}); }))
: null;
var bind$$1 = el.attrsMap['v-bind'];
if ((attrs || bind$$1) && !children) {
res += ",null";
}
if (attrs) {
res += "," + attrs;
}
if (bind$$1) {
res += (attrs ? '' : ',null') + "," + bind$$1;
}
return res + ')'
}
// _t("header", children, attrs)
// 最终code
with(this) {
return _c('div',{
staticClass:"container"
},[
_c('header',[_t("header")],2),
_c('main',[_t("default",[_v("默认内容")])],2),
_c('footer',[_t("footer")],2)
]
)
}

运行时实现(子组件)

  • 普通slot渲染时, 通过$slot返回父组件传过来的children vnode.
  • 运行(调用render方法)时通过_t(renderSlot)生成vnode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// _t方法
function renderSlot : vnode(
name,
fallback, // slot包裹内容
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name]; // scope-slot的render方法
var nodes;
if (scopedSlotFn) { // 2.6后scope-slot和slot都在scopedSlots里
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 { // 兼容2.5
nodes = this.$slots[name] || fallback; // 默认内容
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}

2.6里slot和scope slot统一到scope slot, 优先从$scopedSlots获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Vue.prototype._render = function () { // 子组件render
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots( // $scopedSlots 来源
_parentVnode.data.scopedSlots,
vm.$slots, // 兼容2.5
vm.$scopedSlots
);
}
}
function normalizeScopedSlots (
slots, // 原父组件scope slot
normalSlots, // 普通$slots
prevSlots // 反复刷新
) {
var res;
var hasNormalSlots = Object.keys(normalSlots).length > 0;
var isStable = slots ? !!slots.$stable : !hasNormalSlots;
var 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 (var key$1 in slots) {
if (slots[key$1] && key$1[0] !== '$') {
res[key$1] = normalizeScopedSlot(normalSlots, key$1, slots[key$1]);
}
}
}
// expose normal slots on scopedSlots
for (var key$2 in normalSlots) {
if (!(key$2 in res)) {
res[key$2] = proxyNormalSlot(normalSlots, key$2); // 代理了vnode
}
}
// 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)._normalized = res;
}
def(res, '$stable', isStable);
def(res, '$key', key);
def(res, '$hasNormal', hasNormalSlots);
return res
}

$slots来源

  • 父组件调用$mount=>编译模板=>父组件渲染调用_render生成vnode
  • 生成app-layout的vnode时, 先创建组件里面的children vnode作为参数传入
  • 创建占位符节点后, patch vnode.
  • patch时创建组件实例createChildren => componentVNodeHooks => createComponentInstanceForVnode=> 子组件new vue
    • 子组件流程: init => initRender => resolveSlots

父组件提供

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Vue.prototype._render = function () {
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject; // 子组件获取父组件scopedSlots
}
vnode = render.call(vm._renderProxy, vm.$createElement);
// _c('app-layout',...) createElement
}
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
vnode = createComponent(Ctor, data, context, children, tag);
}
function createComponent ( // 创建组件vnode
Ctor,
data,
context, // _renderProxy
children,
tag
) {
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
}
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
createElm(vnode, insertedVnodeQueue, parentElm, refElm);
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}

子组件获取

  • 流程: init => initRender => resolveSlots
  • 2.6后的render里适配成$scopedSlots
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Vue.prototype._init = function (options) {
function initInternalComponent (vm, options) {
opts._parentVnode = parentVnode;
opts._renderChildren = vnodeComponentOptions.children;
}
initLifecycle(vm);
initEvents(vm);
initRender(vm);
}
function initRender (vm: Component) {
// ...
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context // 父组件vm做slot的context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
}
/**
* Runtime helper for resolving raw children VNodes into a slot object.
*/
function resolveSlots (
children,
context // 父组件vm
) {
if (!children || !children.length) {
return {}
}
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var 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
) { // 命名slot
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else { // 默认slot
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}

scope-slot

父组件parse gencode

  • 添加属性el.slotScope = slotScope;
  • 添加属性slotTarget slot="xxx"如果有的话
  • 在closeElement里除了把节点添加到ast parent的children中(之后又删除), 还增加scopedSlots属性 currentParent.scopedSlots[name] = element
  • 生成的render里没有children而是一个函数fn: function(props) {}, 参数就是slot-scope="props"里面props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent (el) { // 2.6
var slotScope;
if (el.tag === 'template') {
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
el.slotScope = slotScope;
}
// slot="xxx"
var slotTarget = getBindingAttr(el, 'slot');
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget;
}
// 2.6 v-slot syntax
{
// ... v-slot:bind可以是scope props或scope target
}
}
function closeElement (element) {
if (currentParent && !element.forbidden) {
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;
}
currentParent.children.push(element); // 2.5不会把element插入children, 2.6在父element里删除
element.parent = currentParent;
}
}
element.children = element.children.filter(function (c) { return !(c).slotScope; });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function genData$2 (el, state) {
var data = '{';
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += "slot:" + (el.slotTarget) + ",";
}
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
// component
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function genScopedSlots (
el,
slots,
state
) {
var generatedSlots = Object.keys(slots)
.map(function (key) { return genScopedSlot(slots[key], state); })
.join(',');
return ("scopedSlots:_u([" + generatedSlots + "]" + (needsForceUpdate ? ",null,true" : "") + (!needsForceUpdate && needsKey ? (",null,false," + (hash(generatedSlots))) : "") + ")")
}
// {
// scopedSlots: _u([
// {
// key: "default",
// fn: function(props) {
// return [
// _c("p", [_v("Hello from parent " + _s(hello))]), // hello是父vm实例的上的
// _c("p", [_v(_s(props.text + props.msg))])
// ];
// }
// }
// ]);
// }
function genScopedSlot (
el,
state
) {
var 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)
}
var slotScope = el.slotScope === emptySlotScopeToken
? ""
: String(el.slotScope);
var 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
var reverseProxy = slotScope ? "" : ",proxy:true";
// "function(props){return [_c('p',[_v("Hello from parent")]),_c('p',[_v(_s(props.text + props.msg))])]}"
// {key: 'default', fn: xxx}
return ("{key:" + (el.slotTarget || "\"default\"") + ",fn:" + fn + reverseProxy + "}")
}

父组件运行时/render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function resolveScopedSlots (
fns, // see flow/vnode
res,
// the following are added in 2.6
hasDynamicKeys,
contentHashKey
) {
res = res || { $stable: !hasDynamicKeys };
for (var i = 0; i < fns.length; i++) {
var 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).$key = contentHashKey;
}
return res
}
// scopedSlots: {
// default: renderƒn (props)
// }

子组件gencode render

  • genslot时获取slot上绑定的props
  • render时先获取$scopedSlots, 在执行_t(renderSlot)
  • scopedSlotFn中parent的属性在还是从parent获取, 而slot props从子组件传入的slot props获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function genSlot (el, state) {
var slotName = el.slotName || '"default"';
var children = genChildren(el, state); // slot包裹子节点
var res = "_t(" + slotName + (children ? ("," + children) : '');
var attrs = el.attrs || el.dynamicAttrs // 属性的生成 "{"text":"Hello ","msg":msg}"
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(function (attr) { return ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
}); }))
: null;
? genProps
}
// with (this) {
// return _c(
// "div",
// { staticClass: "child" },
// [_t("default", null, { text: "Hello ", msg: msg })], // 执行render是获取了msg
// 2
// );
// }
  • 子组件_render时获取$scopedSlots, 是父组件提供data属性插入的
1
2
3
4
5
6
7
8
9
Vue.prototype._render = function () { // 子组件render
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots( // $scopedSlots 来源
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// _t执行
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
}
}

父组件更新子组件slot

  • patchVnode => prepatch =>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function patchVnode () {
    var i;
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode);
    }
    }
    prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
    );
    },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function updateChildComponent (
vm,
propsData,
listeners,
parentVnode,
renderChildren
) {
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren.
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
var newScopedSlots = parentVnode.data.scopedSlots;
var oldScopedSlots = vm.$scopedSlots;
var hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
);
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
var needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
);
vm.$options._parentVnode = parentVnode;
vm.$vnode = parentVnode; // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode;
}
vm.$options._renderChildren = renderChildren;
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject;
vm.$listeners = listeners || emptyObject;
// update props
// update listeners
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context);
vm.$forceUpdate();
}
}