Vue.js源码分析(⼆⼗五)⾼级应⽤插槽详解
我们定义⼀个组件的时候,可以在组件的某个节点内预留⼀个位置,当⽗组件调⽤该组件的时候可以指定该位置具体的内容,这就是插槽的⽤法,⼦组件模板可以通过slot标签(插槽)规定对应的内容放置在哪⾥,⽐如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="cdn.jsdelivr/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<app-layout>
<h1 slot="header">{{title}}</h1>
<p>{{msg}}</p>
<p slot="footer"></p>
</app-layout>
</div>
</div>
<script>
Vueponent('AppLayout',{                                    //⼦组件,通过slot标签预留了三个插槽,分别为header、默认插槽和footer插槽
template:`<div class="container">
<header><slot name="header"></slot></header>
<main><slot>默认内容</slot></main>
<footer><slot name="footer"><h1>默认底部</h1></slot></footer>
</div>`
})
new Vue({
el: '#app',
template:``,
data:{
title:'我是标题',msg:'我是内容'
}
})
</script>
</body>
</html>
渲染结果为:
对应的html节点如下:
引⽤AppLayout这个组件时,我们指定了header和footer这两个插槽的内容
对于普通插槽来说,插槽⾥的作⽤域是⽗组件的,例如⽗组件⾥的<h1 slot="header">{{title}}</h1>,⾥
⾯的{{title}}是在⽗组件定义的,如果需要使⽤⼦组件的作⽤域,可以使⽤作⽤域插槽来实现,我们下⼀节再讲解作⽤域插槽。
源码分析
Vue内部对插槽的实现原理是⼦组件渲染模板时发现是slot标签则转换为⼀个_t函数,然后把slot标签⾥的内容也就是⼦节点VNode的集合作为⼀个_t函数的参数,_t等于Vue全局的renderSlot()函数。
插槽的实现先从⽗组件实例化开始,如下:
⽗组件解析模板将模板转换成AST对象时会执⾏processSlot函数,如下:
function processSlot (el) {        //第9467⾏解析slot插槽
if (el.tag === 'slot') {                          //如果是slot标签(普通插槽,⼦组件的逻辑))
/*略*/
} else {
var slotScope;
if (el.tag === 'template') {                                        //如果标签名为template(作⽤域插槽的逻辑)
/*略*/
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {      //然后尝试获取slot-scope属性(作⽤域插槽的逻辑)
/*略*/
}
var slotTarget = getBindingAttr(el, 'slot');                        //尝试获取slot特性        ;例如例⼦⾥的<h1 slot="header">{{title}}</h1>会执⾏到这⾥
if (slotTarget) {                                                  //如果获取到了
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget;        //则将值保存到el.slotTarget⾥⾯,如果不存在,则默认为default
// preserve slot as an attribute for native shadow DOM compat
/
/ only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {                        //如果当前不是template标签且 el.slotScoped⾮空
addAttr(el, 'slot', slotTarget);                                        //则给el.slot增加⼀个ieslotTarget属性
}
}
}
}
执⾏到这⾥后如果⽗组件某个节点有⼀个slot的属性则会新增⼀个slotTarget属性,例⼦⾥的⽗组件解析完后对应的AST对象如下:
接下来在generate将AST转换成render函数执⾏genData$2获取data属性时会判断如果AST.slotTarget存在且el.slotScope不存在(即是普通插槽,⽽不是作⽤域插槽),则data上添加⼀个slot属性,值为对应的值,如下:
function genData$2 (el, state) {    //第10274⾏
/*略*/
if (el.slotTarget && !el.slotScope) {        //如果el有设置了slot属性且 el.slotScope为false
data += "slot:" + (el.slotTarget) + ",";        //则拼凑到data⾥⾯
}
/*略*/
}
例⼦⾥的⽗组件执⾏到这⾥对应的rendre函数如下:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_c('app-layout',[_c('h1',{attrs:{"slot":"header"},slot:"header"},[_v(_s(title))]),_v(" "),_c('p',[_v(_s(msg))]),_v(" "),_c('p',{attrs:{"slot":"footer"},slot:"footer"})])],1)])}
这样看得不清楚,我们把render函数整理⼀下,如下:
with(this) {
return _c('div', {attrs: {"id": "app"}},
[_c('div',
[_c('app-layout',
[
_c('h1', {attrs: {"slot": "header"},slot: "header"},[_v(_s(title))]),
_v(" "),
_c('p', [_v(_s(msg))]),
_v(" "),
_c('p', {attrs: {"slot": "footer"},slot: "footer"})
])
]
,
1)
]
)
}
我们看到引⽤⼀个组件时内部的⼦节点会以⼀个VNode数组的形式传递给⼦组件,由于函数是从内到外执⾏的,因此该render函数渲染时会先执⾏⼦节点VNode的⽣成,然后再调⽤_c('app-layout', ...)去⽣成⼦组件VNode
⽗组件创建⼦组件的占位符VNode时会把⼦节点VNode以数组形式保存到占位符VNodeponentOptions.children属性上。
接下来是⼦组件的实例化过程:
⼦组件在解析模板将模板转换成AST对象时也会执⾏processSlot()函数,如下:
function processSlot (el) {        //第9467⾏解析slot插槽
if (el.tag === 'slot') {              //如果是slot标签(普通插槽,⼦组件的逻辑))
el.slotName = getBindingAttr(el, 'name');        //获取name,保存到slotName⾥⾯,如果没有设置name属性(默认插槽),则el.slotName=undefined
if ("development" !== 'production' && el.key) {
warn$2(
"`key` does not work on <slot> because slots are abstract outlets " +
"and can possibly expand into multiple elements. " +
"Use the key on a wrapping element instead."
);
}
} else {
/*略*/
}
}
接下来在generate将AST转换成rende函数时,在genElement()函数执⾏的时候如果判断当前的标签是slot标签则执⾏genSlot()函数,如下:
function genSlot (el, state) {      //第10509⾏渲染插槽(slot节点)
var slotName = el.slotName || '"default"';            //获取插槽名,如果未指定则修正为default
var children = genChildren(el, state);                //获取插槽内的⼦节点
var res = "_t(" + slotName + (children ? ("," + children) : '');      //拼凑函数_t
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 + ')'                                  //最后返回res字符串
}
通过genSlot()处理后,Vue会把slot标签转换为⼀个_t函数,⼦组件渲染后⽣成的render函数如下:
with(this){return _c('div',{staticClass:"container"},[_c('header',[_t("header")],2),_v(" "),_c('main',[_t("default",[_v("默认内容")])],2),_v(" "),_c('footer',[_t("footer",[_c('h1',[_v("默认底部")])])],2)])}
这样看得也不清楚,我们把render函数整理⼀下,如下:
with(this) {
return _c('div', {staticClass: "container"},
[
_c('header', [_t("header")], 2),
_v(" "),
_c('main', [_t("default", [_v("默认内容")])], 2),
_v(" "),
_c('footer', [_t("footer", [_c('h1', [_v("默认底部")])])], 2)
]
)
}
可以看到slot标签转换成_t函数了。
vue中reactive接下来是⼦组件的实例化过程,实例化时⾸先会执⾏_init()函数,_init()函数会执⾏initInternalComponent()进⾏初始化组件函数,内部会将占位符
VNodeponentOptions.children保存到⼦组件实例vm.$options._renderChildren上,如下:
function initInternalComponent (vm, options) {      //第4632⾏⼦组件初始化⼦组件
var opts = vm.$options = structor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
opts._parentElm = options._parentElm;
opts._refElm = options._refElm;
var vnodeComponentOptions = parentVnodeponentOptions;    //占位符VNode初始化传⼊的配置信息
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;        //调⽤该组件时的⼦节点,在插槽、内置组件⾥中会⽤到
opts._componentTag = vnodeComponentOptions.tag;
if (der) {
opts.staticRenderFns = options.staticRenderFns;
}
}
执⾏到这⾥时例⼦的_renderChildren等于如下:
这就是我们在⽗组件内定义的⼦VNode集合,回到_init()函数,随后会调⽤initRender()函数,该函数会调⽤resolveSlots()解析vm.$options._renderChildren并保存到⼦组件实例
vm.$slots属性上如下:
function initRender (vm) {              //第4471⾏初始化渲染
vm._vnode = null; // the root of the child tree
vm._staticTrees = null; // v-once cached trees
var options = vm.$options;
var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
var renderContext = parentVnode && t;
vm.$slots = resolveSlots(options._renderChildren, renderContext);        //执⾏resolveSlots获取占位符VNode下的slots信息,参数为占位符VNode⾥的⼦节点,执⾏后vm.$slots格式为:{default:[...],footer:[VNode],header:[VNode]}  vm.$scopedSlots = emptyObject;
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
var parentData = parentVnode && parentVnode.data;
/* istanbul ignore else */
{
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
!isUpdatingChildComponent && warn("$attrs is readonly.", vm);
}, true);
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
!isUpdatingChildComponent && warn("$listeners is readonly.", vm);
}, true);
}
}
resolveSlots会解析每个⼦节点,并将⼦节点保存到$slots属性上,如下:
function resolveSlots (        //第4471⾏分解组件内的⼦组件
children,                        //占位符Vnode⾥的内容
context                          // context:占位符Vnode所在的Vue实例
) {
var slots = {};                      //缓存最后的结果
if (!children) {                      //如果引⽤当前组件时没有⼦节点,则返回空对象
return slots
}
for (var i = 0, l = children.length; i < l; i++) {        //遍历每个⼦节点
var child = children[i];                                        //当前的⼦节点
var data = child.data;                                          //⼦节点的data属性
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {                    //如果data.attrs.slot存在    ;例如:"slot": "header"
delete data.attrs.slot;                                          //则删除它
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((t === context || child.fnContext === context) &&  //如果该⼦节点有data属性且data.slot⾮空,即设置了slot属性时
data && data.slot != null
) {
var name = data.slot;                                                  //获取slot的名称
var slot = (slots[name] || (slots[name] = []));                        //如果slots[name]不存在,则初始化为⼀个空数组
if (child.tag === 'template') {                                      //如果tag是⼀个template
slot.push.apply(slot, child.children || []);
} else {                                                              //如果child.tag不是template
slot.push(child);                                                      //则push到slot⾥⾯(等于外层的slots[name])
}
} 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                              //最后返回slots
}
例⼦⾥的⼦组件执⾏完后$slot等于:
可以看到:slot是⼀个对象,键名对应着slot标签的name属性,如果没有name属性,则键名默认为default,值是⼀个VNode数组,对应着插槽的内容
最后执⾏_t函数,也就是全局的renderSlot函数,该函数就⽐较简单了,如下:
function renderSlot (          //第3725⾏渲染插槽
name,                                            //插槽名称
fallback,                                        //默认⼦节点
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;                                            //定义⼀个局部变量,⽤于返回最后的结果,是个VNode数组
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if ("development" !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
props = extend(extend({}, bindObject), props);
}
nodes = scopedSlotFn(props) || fallback;
} else {
var slotNodes = this.$slots[name];                  //先尝试从⽗组件那⾥获取该插槽的内容,this.$slots就是上⾯⼦组件实例化时⽣成的$slots对象⾥的信息
// warn duplicate slot usage
if (slotNodes) {                                    //如果该插槽VNode存在
if ("development" !== 'production' && slotNodes._rendered) {  //如果该插槽已存在(避免重复使⽤),则报错
warn(
"Duplicate presence of slot \"" + name + "\" found in the same render tree " +
"- this will likely cause render errors.",
this
);
}
slotNodes._rendered = true;                              //设置slotNodes._rendered为true,避免插槽重复使⽤,初始化执⾏_render时会将每个插槽内的_rendered设置为false的
}
nodes = slotNodes || fallback;                      //如果slotNodes(⽗组件⾥的插槽内容)存在,则保存到nodes,否则将fallback保存为nodes
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes                                        //最后返回nodes
}
}
OK,搞定。
注:有段时间没看Vue源码了,还好平时有在做笔记,很快就理解了,不管什么框架,后端也是的,语⾔其实不难,难的是理解框架的设计思想,从事程序员这⼀⾏因为要学的东西很多,我们也不可能每个去记住的,所以笔记很重要。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。