在Vue 2.6.0 中,咱們爲具名插槽和做用域插槽引入了一個新的統一的語法(即v-slot指令)。它取代了 slot和slot-scope。vue
父子組件分別以node
<app-layout>另外一個主要段落</app-layout>
複製代碼
和數組
<div class="container"><slot></slot></div>
複製代碼
爲例。 父組件生成的render函數爲:bash
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('app-layout',[_v("另外一個主要段落")])],1)}
})
複製代碼
調用:app
// Ctor爲app-layout的子組件構造函數
vnode = createComponent(Ctor, data, context, children, tag);
// 建立Vnode
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
);
複製代碼
app-layout組件生成的Vnode爲:async
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options)
children: [VNode],
listeners: undefined,
propsData: undefined,
tag: "app-layout"
},
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {on: undefined, hook: {…}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
複製代碼
app-layout的子組件生成的render函數爲:函數
(function anonymous(
) {
with(this){return _c('div',{staticClass:"container"},[_t("default")],2)}
})
複製代碼
this指向app-layout父組件的構造函數。在子組件初始化過程當中的initRender會對slot進行以下處理:ui
vm.$slots = resolveSlots(options._renderChildren, renderContext);
複製代碼
其中options._renderChildren表示一個包含app-layout子組件VNode的數組,具體函數爲this
function resolveSlots (
children,
context
) {
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
) {
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 {
(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
}
複製代碼
返回{default: [VNode]}
,此時vm.$slot = {default: [VNode]}
和vm.$scopedSlots = emptyObject
,接下來進入編譯階段,其中的默認插槽slot被genSlot函數解析成spa
_t("default")
複製代碼
在子組件中的_render函數中會格式化插槽的表示
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
複製代碼
生成
vm.$scopedSlots = {
default: ƒ (),
$hasNormal: true,
$key: undefined,
$stable: false
}
複製代碼
接着看_t函數:
// name1爲default, 其餘爲undefined
function renderSlot (
name,
fallback,
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (!isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
props = extend(extend({}, bindObject), props);
}
nodes = scopedSlotFn(props) || fallback;
} else {
// 從this.$slots中獲取default值,是app-layout組件的text虛擬節點內容[VNode]
nodes = this.$slots[name] || fallback;
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
複製代碼
接着將子組件內容掛載到父級,而後掛載到body上,最後再刪除初始標籤內容。具名插槽同理,只是將default改爲對應的名字xx。
父子組件分別以
<div id="app"><app-layout :items="items"><template slot-scope="list"><div>{{ list.data }}</div></template></app-layout></div>
複製代碼
和
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
複製代碼
爲例。 在模板解析template尾標籤時,執行closeElement會對template模板組件會做以下處理:
function processSlotContent (el) {
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope');
/* istanbul ignore if */
if (slotScope) {
warn$2(
"the \"scope\" attribute for scoped slots have been deprecated and " +
"replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " +
"can also be used on plain elements in addition to <template> to " +
"denote scoped slots.",
el.rawAttrsMap['scope'],
true
);
}
// 此時 el.slotScope = 'list'
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
}
}
複製代碼
此時會在element元素上掛載: { slotScope: "list" } 接着closeElement函數繼續處理:
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);
element.parent = currentParent;
複製代碼
生成:
{
scopedSlots: {
"default": {
attrsList: []
attrsMap: {slot-scope: "list"},
children: [{…}],
end: 106,
parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: false,
rawAttrsMap: {
slot-scope: {
end: 68,
name: "slot-scope",
start: 51,
value: "list"
}
},
slotScope: "list",
start: 41,
tag: "template",
type: 1
}
}
}
複製代碼
生成代碼過程當中通過genScopedSlots, genScopedSlot處理
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";
return ("{key:" + (el.slotTarget || "\"default\"") + ",fn:" + fn + reverseProxy + "}")
}
複製代碼
生成
"{ attrs:{"items":items}, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])]}}]),"
複製代碼
完整的render函數爲:
with(this){
return _c('div',
{
attrs:{"id":"app"}
},
[
_c('app-layout',{
attrs:{"items":items},
scopedSlots:_u([{key:"default", fn:function(list){
return [_c('div',[_v(_s(list.data))])]
}}])
})
] ,1)
}
複製代碼
其中_u函數表示
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
}
複製代碼
fns爲[{fn: ƒ (list), key: "default"}]
,該函數返回
{
$stable: true,
default: ƒ (list)
}
複製代碼
app-layout生成的VNode爲:
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options),
children: undefined,
listeners: undefined,
propsData: {items: Array(1)},
tag: "app-layout"
},
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {
attrs: {},
scopedSlots: {
$stable: true,
default: ƒ (list)
},
on: undefined, hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
複製代碼
layout子組件的render函數爲:
with(this){
return _c(
'div',
[
_l((items),function(item, index){
return _t("default", null,{"data": item.text})
})
],
2)
}
複製代碼
_l函數爲:
function renderList (
val,
render
) {
var ret, i, l, keys, key;
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length);
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i);
}
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i);
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = [];
var iterator = val[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
ret.push(render(result.value, ret.length));
result = iterator.next();
}
} else {
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
ret[i] = render(val[key], key, i);
}
}
}
if (!isDef(ret)) {
ret = [];
}
(ret)._isVList = true;
return ret
}
複製代碼
對數組的每個元素,執行:
return _t("default", null,{"data": item.text})
複製代碼
此過程會觸發getter,添加依賴,_t函數爲:
// name爲default,props爲{data: 'text1'}
function renderSlot (
name,
fallback,
props,
bindObject
) {
// f函數
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (!isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
props = extend(extend({}, bindObject), props);
}
// 獲取渲染後的[VNode]
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
}
}
複製代碼
接着將生成的VNode生成元素節點,並將子組件內容掛載到父級,而後掛載到body上,最後再刪除初始標籤內容。 如今咱們來對比一下普通插槽和做用域插槽的區別:// 普通插槽
slots: {
xxoo: h('div')
}
// 做用域插槽
scopedSlots: {
xxoo: (scopedData) => h('div', scopedData.a)
}
複製代碼
做用域插槽和普通插槽的區別在於,子組件拿到它的時候它仍是一個函數,只有你執行該函數,它纔會返回要渲染的內容(即vnode),因此能夠統一爲:
// 普通插槽
slots: {
xxoo: () => h('div')
}
複製代碼
這個也是Vue2.6.*中的改變之處。
父子組件分別以
<div id="app"><app-layout :items="items"><template v-slot:default="list"><div>{{ list.data }}</div></template></app-layout></div>
複製代碼
和
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
複製代碼
爲例。 父組件在模板解析階段,編譯閉合標籤template時,closeElement函數中會執行processSlotContent,以下:
// 2.6 v-slot syntax
{
if (el.tag === 'template') {
// v-slot on <template>
// 做用在template上,slotRE = /^v-slot(:|$)|^#/
var slotBinding = getAndRemoveAttrByRegex(el, slotRE);
if (slotBinding) {
var ref = getSlotName(slotBinding);
var name = ref.name;
var dynamic = ref.dynamic;
el.slotTarget = name;
el.slotTargetDynamic = dynamic;
el.slotScope = slotBinding.value || emptySlotScopeToken; // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
var slotBinding$1 = getAndRemoveAttrByRegex(el, slotRE);
if (slotBinding$1) {
// add the component's children to its default slot var slots = el.scopedSlots || (el.scopedSlots = {}); var ref$1 = getSlotName(slotBinding$1); var name$1 = ref$1.name; var dynamic$1 = ref$1.dynamic; var slotContainer = slots[name$1] = createASTElement('template', [], el); slotContainer.slotTarget = name$1; slotContainer.slotTargetDynamic = dynamic$1; slotContainer.children = el.children.filter(function (c) { if (!c.slotScope) { c.parent = slotContainer; return true } }); slotContainer.slotScope = slotBinding$1.value || emptySlotScopeToken; // remove children as they are returned from scopedSlots now el.children = []; // mark el non-plain so data gets generated el.plain = false; } } } 複製代碼
該函數在element元素中添加屬性:
{
...
slotScope: "list",
slotTarget: ""default"",
slotTargetDynamic: false,
...
}
複製代碼
而後在app-layout組件的closeElement函數中增長屬性:
...
scopedSlots: {"default": {
attrsList: [],
attrsMap: {v-slot:default: "list"},
children: [{
attrsList: [],
attrsMap: {},
children: [{…}],
end: 99,
parent: {type: 1, tag: "template", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: true,
rawAttrsMap: {},
start: 73,
tag: "div",
type: 1}],
end: 110,
parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: false,
rawAttrsMap: {v-slot:default: {
end: 72,
name: "v-slot:default",
start: 51,
value: "list"
}},
slotScope: "list",
slotTarget: ""default"",
slotTargetDynamic: false,
start: 41,
tag: "template",
type: 1
}}
...
複製代碼
接着在生成代碼階段,genElement中的genData$2函數會處理上面生成的scopedSlots屬性值:
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
複製代碼
該函數具體爲
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))) : "") + ")")
}
複製代碼
genScopedSlot函數的做用是生成以下代碼:
"{key:"default",fn:function(list){return [_c('div',[_v(_s(list.data))])]}}"
複製代碼
返回了
"{ attrs: { "items":items }, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])] } }]),"
複製代碼
生成完整的render函數爲:
with(this){
return _c('div',
{attrs:{"id":"app"}},
[_c('app-layout',{
attrs:{"items":items},
scopedSlots:_u([{
key:"default",
fn:function(list){return [_c('div',[_v(_s(list.data))])]}}])
})
],1)
}
複製代碼
生成VNode時,_u即resolveScopedSlots函數處理
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
}
複製代碼
其中函數參數fns=[{key: "default", fn: ƒ}]
和res={$stable: true}
。返回值爲 {$stable: true, default: ƒ (list)}
生成虛擬DOM節點的函數vnode = createComponent(Ctor, data, context, children, tag)
,其中data爲
{
attrs: {},
hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
on: undefined,
scopedSlots: {
$stable: true,
default: ƒ (list)
}
}
複製代碼
app-layout的VNode爲:
{
syncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options)
children: undefined,
listeners: undefined,
propsData: {},
tag: "app-layout",
}
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {attrs: {}, scopedSlots: {
$stable: true,
default: ƒ (list)
}, on: undefined, hook: {…}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
複製代碼
子組件與slot-scope一致的render函數爲:
with(this){return _c('div',[
_l((items),function(item,index){
return _t("default",null,{"data":item.text})
})],2)
}
複製代碼
一樣的通過_t函數處理
function renderSlot (
name,
fallback,
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (!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
}
}
複製代碼
this.$scopedSlots爲:
{
default: ƒ (),
$hasNormal: false,
$key: undefined,
$stable: true
}
複製代碼
props爲{data: "text1"}
,返回的nodes爲子組件的[VNode],最後掛載子組件到父組件,並將父級組件掛載到body頁面,再刪除老的元素節點。此處理過程與slot-scope一致。 匿名插槽模板
<div id="app"><app-layout v-slot:default>另外一個主要段落</app-layout></div>
複製代碼
會被編譯爲:
"_c('app-layout',{ scopedSlots:_u([{key:"default",fn:function(){return [_v("另外一個主要段落")] },proxy:true}]) })"
複製代碼
其餘處理過程一致。
在Vue2.5.*以前,若是是普通插槽就直接訪問的是VNode,而若是是做用域插槽,因爲子組件須要在父組件訪問子組件的數據,因此父組件下是一個未執行的函數(slotScope) => return h('div',slotScope.msg),接受子組件的slotProps參數,在子組件渲染實例時會調用該函數傳入數據。在2.6以後,二者合併,普通插槽也變成一個函數,只是不接受參數了。