前言
本篇文章slots的知识点进行Vue的源码逻辑梳理,旨在理解slots背后的实现逻辑。
实际上本篇文章梳理slots相关的逻辑主要是2点:
Slot具体逻辑梳理
结合简单实例来梳理slots相关知识,实例如下:
<div id="app">
<element-block>测试</element-block>
</div>
<script>
Vue.component('element-block', {
template: '<div>Vue之slot <slot></slot></div>'
});
const vm = new Vue({
el: '#app'
});
</script>
上面的简单实例涉及到两个组件:根组件和已定义组件element-block。
构建render时slot的处理
实际上这部分是在 Vue解析构建Render函数 文章中对解析流程中对于标签和文本有了一定的介绍,实际上这边处理的情况还有注释、标签结束等相关的处理都没有介绍到(之后会对这篇文章进行重写),这里就先看看这个步骤中对<slot>的处理吧。
实际上对于<slot>也是按照标签的处理方式,从上面文章提及的逻辑中实际上这边逻辑如下:
而在processElement函数中实际上是对几种特殊指令进行特殊处理,具体指令如下:
对于<slot>标签还是按照普通标签来解析,只是当解析到是标签就需要调用processElement,而在该函数中就需要对slot进行特殊的处理,具体的处理逻辑如下(这里只看slot的处理):
if (el.tag === 'slot') {
// 获取slot中name属性值
el.slotName = getBindingAttr(el, 'name');
}
整个解析完成完成之后element-block组件对应的ast结构如下:
{
type: 1,
tag: "div",
plain: true,
attrsList: [],
attrsMap: {},
parent: undefined,
children:[
{
type: 3,
text: 'Vue之slot '
},
{
type: 1,
tag: 'slot',
...
}
]
}
可知,slot标签在解析的时候是按照标签的形式来解析的。而构建成render函数slot与普通的标签是存在区别的,而这个区别就是本文中第二个问题的源头。
稍微提及下构建render函数中构建函数体的处理generate函数的逻辑,逻辑脉络如下:
genSlot函数相关逻辑如下:
function genSlot (el, state) {
// 默认名称的由来
var slotName = el.slotName || '"default"';
// 循环处理slot下的子组件
var children = genChildren(el, state);
// 注意这里的_t,这是核心
var res = "_t(" + slotName + (children ? ("," + children) : '');
// 其他属性相关的处理
var attrs = el.attrs && ("{" + (el.attrs.map(function (a) { return ((camelize(a.name)) + ":" + (a.value)); }).join(',')) + "}");
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,在之前的文章涉及到了_c、_s、_v,这里就在汇总下:
而实例element-block自定义组件构建成的render函数的函数体如下:
with(this){return _c('div',[_v("Vue之slot "),_t("default")],2)}
_t是renderSlot函数,即本文第二点的具体逻辑了。
slot插槽数据合并
当render函数被解析创建出来之后,这边的逻辑就是在render函数被调用时触发,实际上这里也就是调用_t函数即renderSlot函数。该函数的主要逻辑如下(slot相关的):
function renderSlot (name, fallback, props, bindObject) {
var slotNodes = this.$slots[name];
if (slotNodes) {
slotNodes._rendered = true;
}
// 其他作用域slot的逻辑暂不提及
return slotNodes || fallback;
}
从上面逻辑中,可以得到关键信息:
那此时就需要确认slots中保存的slot相关数据了。全局搜索Vue源码发现对$slots属性进行赋值的操作不过就3处地方,列举如下:
这里需要排除下到底是触发哪个函数对slots进行赋值的,依据本文的实例debugger,发现slots就是在element-block自定义组件初始化过程中处理的,即initRender函数中处理。
而initRender函数中处理$slots逻辑如下:
vm.$slots = resolveSlots(options._renderChildren, renderContext);
这里需要注意的是renderContext是父VNode对象的context即Vue实例。
function resolveSlots (children, context) {
var slots = {};
if (!children) return slots
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// 带有名称的slot,data以及data.attrs都会存在slot属性
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// 处理带有名称的slot
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
// slot属性所在标签是template
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 默认slot
(slots.default || (slots.default = [])).push(child);
}
}
// 处理仅包含空格的vnode
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}
从这边实际上从另一个角度更加深刻理解了官网下的这句话:
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
上面这句话另一个解读就是:
正是element-block子组件的编译过程处理了和收集了当前Vue实例的$slots。通过_t函数(即renderSlot合并了插槽数据)。
作用域slot的特殊处理
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。而作用域slot就是将父组件数据传递到子组件中,以便子组件在编译时调用。
作用域slot整个流程梳理的实例如下:
<div id="app">
<element-block>
<span slot-scope="scope">{{scope.data}}</span>
</element-block>
</div>
<script>
Vue.component('element-block', {
template: '<div><slot :data="text"></slot></div>',
data() {
return {
text: 'scoped-slot'
};
}
});
const vm = new Vue({
el: '#app'
});
</script>
构建render函数作用域slot的处理
这边的主要逻辑还是跟普通的slot相同,主要的处理函数是processElement下processSlot,这里就具体看看processSlot针对作用域slot的处理逻辑,主要逻辑代码如下:
function processSlot (el) {
var slotScope;
// <template slot-scope>
if (el.tag === 'template') {
// 支持scope 和 slot-scope
slotScope = getAndRemoveAttr(el, 'scope');
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
el.slotScope = slotScope;
}
// 判断是否存在slot元素, 这里Vue源码给了解释
// 针对原生的shadow DOM保留slot属性, 但限于非范围的slot
var slotTarget = getBindingAttr(el, 'slot');
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget;
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget);
}
}
}
从上面主要逻辑可知,会存在slotScope属性,根据debugger发现slotScope的逻辑在parseHTML的start(看普通slot这边的提及)之后的处理逻辑也有涉及到,具体的逻辑是:
if (currentParent && !element.forbidden) {
// 标签上存在v-if、v-else等指令的
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else if (element.slotScope) {
// 处理作用域slot
currentParent.plain = false;
// 默认和带名称的情况
var name = element.slotTarget || '"default"';
(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
从上面可看出会在父元素对象上创建scopedSlots属性(slot-scope属性肯定在一个标签上,所以这里在父元素对象上注册scopedSlot属性)。
构建出了ast结构后,接下来的处理解释创建函数体了,这边的大体逻辑跟上面slot相同,这里需要注意的是逻辑是针对作用域slot的处理不同了:
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el.scopedSlots, state)) + ",";
}
可见作用域slot是调用genScopedSlots函数,而该函数的具体逻辑如下:
function genScopedSlots (slots, state) {
// 构建scopedSlots: _u([...])的结构
return ("scopedSlots:_u([" + (Object.keys(slots).map(function (key) {
return genScopedSlot(key, slots[key], state)
}).join(',')) + "])")
}
内部调用的genScopedSlot函数的具体处理如下:
function genScopedSlot (key, el, state) {
if (el.for && !el.forProcessed) {
return genForScopedSlot(key, el, state)
}
var fn = "function(" + (String(el.slotScope)) + "){" +
"return " + (el.tag === 'template'
? el.if
? ((el.if) + "?" + (genChildren(el, state) || 'undefined') + ":undefined")
: genChildren(el, state) || 'undefined'
: genElement(el, state)) + "}";
// {"key": "default", fn: function(scope) {return 函数体}}
return ("{key:" + key + ",fn:" + fn + "}")
}
这里是比较重要的逻辑,这里会结合实例详细梳理的。首先通过上面的解析构建出来的ast的结构如下:
{
type: 1,
tag: "div",
plain: false,
parent: undefined,
children:[
{
type: 1,
tag: 'element-block',
scopedSlots: {
default: VNode对象
},
children: [
{
type: 1,
tag: 'span',
slotScope: 'scope'
}
]
}
]
}
上面的ast就是实例解析出来的大概结构(还有其他属性这里就不具体了),由这个结构来看genElement的构建过程就比较具体了。
整个解析过程如下:
就实例而言,这边作用域slot会构建成:
{
scopedSlots:_u([
{
key:"default",
fn:function(scope){return _c('span',{},[_v(_s(scope.data))])}
}
]),
}
那么构建出来的render函数的函数体就如下:
with(this){
return _c('div',{attrs:{"id":"app"}},
[
_c('element-block', {
scopedSlots:_u([
{
key:"default",
fn:function(scope){
return _c('span',{},[_v(_s(scope.data))])
}
}
])
})
], 1)
}
作用域slot插槽数据合并
在普通slot中,提及了是render函数执行时调用_t函数(renderSlot)来实现数据的合并,而$slots源在element-block组件initRender时被收集。
那么作用域slot的处理是不是有所区别呢?
作用域slot直接父组件render处理
主要的区别在于element-block所在的父组件的render处理。实际上主要点还是相同即render函数执行触发相关函数执行,只不过作用域slot是_u函数,通过源码可知该函数:
function installRenderHelpers (target) {
target._u = resolveScopedSlots;
}
可见作用域slot是要调用resolveScopedSlots函数来处理,而该函数的具体处理逻辑如下:
function resolveScopedSlots (fns, res) {
res = res || {};
for (var i = 0; i < fns.length; i++) {
if (Array.isArray(fns[i])) {
resolveScopedSlots(fns[i], res);
} else {
// 收集作用域slot
res[fns[i].key] = fns[i].fn;
}
}
return res
}
通过_u函数的执行,实际上就是平铺所有的作用域slot构建成对象:
scopedSlots: {
default: function(scope) {
// 相关处理
}
}
作用域slot自身的组件
在element-block初始化过程中实际上$scopedSlots就是一个冻结的空对象,而真正对其赋值的操作是在element-block对应的组件render函数执行时进行的,关键逻辑如下:
Vue.prototype._render = function () {
// 其他处理
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
}
}
而对应的element-block组件render的函数体实际与普通slot没有什么区别,都是调用_t函数,唯一的区别就是作用域slot会传参,就是子组件中调用的父组件的数据。_t函数就是renderSlot函数的执行,就看下该函数中scopedSlot相关处理逻辑:
function renderSlot (name, fallback, props, bindObject) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
// 作用域slot存在
if (scopedSlotFn) {
props = props || {};
if (bindObject) {
// bindObject应该是一个对象
if ("development" !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
// 调用extend构建props, 实际上就是浅拷贝
props = extend(extend({}, bindObject), props);
}
// 执行作用域slot构建的函数, 生成vnodes
nodes = scopedSlotFn(props) || fallback;
}
}
总结
本篇文章主要梳理了普通slot和作用域slot背后的处理逻辑,了解到:
- initRender -> slots定义−>解析构建生成render函数−>render函数执行−>renderSlot执行−>调用slots -> 生成vnodes
- initRender -> $scopedSlots -> 解析构建生成render函数 -> _render函数执行 -> scopedSlots再赋值−>render函数执行−>调用scopedSlots ->生成vnodes
实际上上面的主要流程是slot所在的组件的主要流程,就本文而言就是element-block组件自身初始化过程,scopedSlots涉及的处理与slots基本相同,不同的在于其父组件下对于两者的解析,不同点如下: