Vue之slot相关
前言本篇文章slots的知识点进行Vue的源码逻辑梳理,旨在理解slots背后的实现逻辑。实际上本篇文章梳理slots相关的逻辑主要是2点:构建成render函数的过程中slot是如何处理的?slot插槽数据是如何合并的?作用域slot的特殊处理Slot具体逻辑梳理结合简单实例来梳理slots相关知识,实例如下:<div id="app">&
前言
本篇文章slots的知识点进行Vue的源码逻辑梳理,旨在理解slots背后的实现逻辑。实际上本篇文章梳理slots相关的逻辑主要是2点:
- 构建成render函数的过程中slot是如何处理的?
- slot插槽内容填充逻辑
- 作用域slot的特殊处理
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的处理逻辑如下:
parseStartTag -> handleStartTag -> options.start ->processElement
而在processElement函数中实际上是对几种特殊指令进行特殊处理,具体指令如下:
- processKey():标签中存在key属性
- processRef():标签中存在ref属性
- processSlot():处理标签是slot
- processComponent():处理标签是组件的
对于<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',
...
}
]
}
从上面的AST结构中可知slot标签在解析的时候是按照标签的形式来解析的,而构建成render函数slot与普通的标签是存在区别的,而这个区别就是本文中第二个问题的源头。
稍微提及下构建render函数中函数体的generate函数的逻辑,逻辑脉络如下:
generate -> genElement -> genData$2 -> 处理各种情况的标签,其中就包括slot标签,调用genSlot函数
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,这里就在汇总下:
_c:是指createElement函数,即创建元素
_s:toString函数
_v:createTextVnode函数,创建文本节点
_t:renderSlot函数,关于slot的,具体的逻辑下面会讲
而实例element-block自定义组件构建成的render函数的函数体如下:
with(this){return _c('div',[_v("Vue之slot "),_t("default")],2)}
_t是renderSlot函数,即本文第二点的具体逻辑了。
slot插槽内容填充
在render函数被调用时,对于插槽slot实际上这里就是调用_t函数即renderSlot函数,该函数的主要逻辑如下(slot相关的):
function renderSlot (name, fallback, props, bindObject) {
var slotNodes = this.$slots[name];
if (slotNodes) {
slotNodes._rendered = true;
}
...
return slotNodes || fallback;
}
从上面逻辑中,可以得到关键信息:
renderSlot函数中关于slot的处理会调用Vue实例的$slots,而该属性是slot的注册中心
从上面逻辑就可以知道,插槽内容的替换来源于$slots。$slots内容是什么时候注册的呢?全局搜索Vue源码发现对$slots属性进行赋值的操作不过就3处地方,列举如下:
- initRender:创建Vue实例时调用
- FunctionalRenderContext:函数式组件的处理逻辑,以当前实例来说基本可以排除
- updateChildComponent:更新子组件,patch阶段调用
依据本文的实例debugger,发现$slots就是在element-block自定义组件初始化过程中处理的,即initRender函数中处理。而initRender函数中处理$slots逻辑如下:
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
}
vm.$slots = resolveSlots(options._renderChildren, renderContext);
从上面代码逻辑实际上可以看出$slots中注册的就是_renderChildren数组的内容,这是slot内容的来源。
options._renderChildren
这实际上涉及到Vue实例创建的处理,在Vue内部实际上对于options是分为两种情况:
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
通过_isComponent区分了两种场景,而_isComponent就有调用createComponentInstanceForVnode函数才会被创建,而该函数在虚拟DOM的init生命周期被调用。
VNode的生命周期函数都是在patch阶段被调用,实际上所有组件实例都是通过createComponentInstanceForVnode来创建(除了根组件的)
而_renderChildren实际上就是对应VNode的children的内容,而children就是template解析成render函数时确认的。
作用域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'
}
]
}
]
}
由这个结构来看genElement的构建过程就比较具体了,之后就是生成render函数的过程。就实例而言,这边作用域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函数构成,可以看出作用域slot是调用_u函数即调用resolveScopedSlots函数来处理,而该函数的具体处理逻辑如下:
function installRenderHelpers (target) {
target._u = 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) {
// 相关处理
}
}
通过_u函数的处理此时scopedSlots就是对象,其内容是在哪里调用的呢?
以实例来说,element-block组件自身的template会被解析成render函数,而scopedSlots的内容就是在其render函数执行时调用
以element-block组件为例,其render函数被构建成:
(function anonymous() {
with(this) {
return _c('div',[_t("default",null,{"data":text})],2)
}
})
其render函数中存在_t函数,就是执行renderSlot中的逻辑了,具体逻辑如下:
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的内容填充来源于$scopedSlots。在组件初始化过程中(即initRender中),$scopedSlots就是一个冻结的空对象,而真正对其赋值的操作是在对应的组件render函数执行时进行的,关键逻辑如下:
Vue.prototype._render = function () {
// 其他处理
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
}
}
总结
本篇文章主要梳理了普通slot和作用域slot背后的处理逻辑,存在插槽的组件背后执行逻辑总结如下:
- initRender -> $slots定义 -> 解析构建生成render函数 -> 视图渲染执行render实例方法 -> template对应的render函数执行 -> renderSlot执行 -> 调用$slots -> 生成vnodes
- initRender -> $scopedSlots定义此时还是空对象 -> 解析构建生成render函数 -> 视图渲染执行render实例方法 -> $scopedSlots赋值 -> template对应的render函数执行 -> renderSlot执行 -> 调用$scopedSlots ->生成vnodes
实际上通过上面两者整个流程,就可以知道$scopedSlots与$slots不同的处理逻辑,主要有如下几点:
普通slot
- 使用slot的组件的父组件的render不需要特殊的构建
- 承载name属性的标签下的所有子标签都是直接解析的
- 在组件初始化时机initRender时就会向$slots中注册相关内容,用于之后的内容填充
作用域slot
- 使用作用域slot的组件需要额外的slot-scope属性传递数据所以存在特殊的构建内容scopedSlots
- 其render函数体中相关使用_u来处理承载slot-scope属性的标签
- 承载slot-scope属性的标签下的所有子标签都是间接解析的,即实际上通过构建了一层函数来处理的
- 在组件初始化时机initRender时其$scopedSlots属性为空对象,在组件render函数执行前才会进行赋值
更多推荐
所有评论(0)