我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过slot标签(插槽)规定对应的内容放置在哪里,比如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/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>
Vue.config.productionTip=false;
Vue.config.devtools=false;
Vue.component('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以数组形式保存到占位符VNode.componentOptions.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函数了。

接下来是子组件的实例化过程,实例化时首先会执行_init()函数,_init()函数会执行initInternalComponent()进行初始化组件函数,内部会将占位符VNode.componentOptions.children保存到子组件实例vm.$options._renderChildren上,如下:

function initInternalComponent (vm, options) {      //第4632行  子组件初始化子组件
var opts = vm.$options = Object.create(vm.constructor.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 = parentVnode.componentOptions; //占位符VNode初始化传入的配置信息
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children; //调用该组件时的子节点,在插槽、内置组件里中会用到
opts._componentTag = vnodeComponentOptions.tag; if (options.render) {
opts.render = options.render;
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 && parentVnode.context;
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 ((child.context === 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
}

writer by:大沙漠 QQ:22969969

例子里的子组件执行完后$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源码了,还好平时有在做笔记,很快就理解了,不管什么框架,后端也是的,语言其实不难,难的是理解框架的设计思想,从事程序员这一行因为要学的东西很多,我们也不可能每个去记住的,所以笔记很重要。

Vue.js 源码分析(二十五) 高级应用 插槽 详解的更多相关文章

  1. Vue.js 源码分析(二十) 指令篇 v-once指令详解

    数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值,例如:<p>Message: {{ msg }}</p>以后每当msg属性发生了改变,插值处的内 ...

  2. Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解

    普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的. 有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单 ...

  3. Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解

    对于过度动画如果要同时渲染整个列表时,可以使用transition-group组件. transition-group组件的props和transition组件类似,不同点是transition-gr ...

  4. Vue.js 源码分析(二十八) 高级应用 transition组件 详解

    transition组件可以给任何元素和组件添加进入/离开过渡,但只能给单个组件实行过渡效果(多个元素可以用transition-group组件,下一节再讲),调用该内置组件时,可以传入如下特性: n ...

  5. Vue.js 源码分析(二十四) 高级应用 自定义指令详解

    除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上 ...

  6. Vue.js 源码分析(二十三) 指令篇 v-show指令详解

    v-show的作用是将表达式值转换为布尔值,根据该布尔值的真假来显示/隐藏切换元素,它是通过切换元素的display这个css属性值来实现的,例如: <!DOCTYPE html> < ...

  7. Vue.js 源码分析(二十一) 指令篇 v-pre指令详解

    该指令会跳过所在元素和它的子元素的编译过程,也就是把这个节点及其子节点当作一个静态节点来处理,例如: <!DOCTYPE html> <html lang="en" ...

  8. Vue.js 源码分析(二十二) 指令篇 v-model指令详解

    Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...

  9. Vue.js 源码分析(十三) 基础篇 组件 props属性详解

    父组件通过props属性向子组件传递数据,定义组件的时候可以定义一个props属性,值可以是一个字符串数组或一个对象. 例如: <!DOCTYPE html> <html lang= ...

随机推荐

  1. Spring Boot 启动以后然后再加载缓存数据 CommandLineRunner

    实际应用中,我们会有在项目服务启动完成以后去加载一些数据或做一些事情(比如缓存)这样的需求. 为了解决这样的问题,Spring Boot 为我们提供了一个方法,通过实现接口 CommandLineRu ...

  2. MongoDB 修改数据Cannot change the size of a document in a capped collection: * != *"

    MongoDB修改数据库数据的时候报错 原因: 集合被设置成了 固定集合 .固定集合的数据不能被修改.只能查找-删除-再插入,也就是创建集合的时候设置了capped参数为true 解决: 创建集合的时 ...

  3. C#上手练习6(方法语句1)

    方法是将完成同一功能的内容放到一起,方便书写和调用的一种方式,也体现了面向对象语言中封装的特性. 定义方法的语法形式如下. 访问修饰符    修饰符    返回值类型    方法名(参数列表){    ...

  4. java基础(12):构造方法、this、super

    1. 构造方法 我们对封装已经有了基本的了解,接下来我们来看一个新的问题,依然以Person为例,由于Person中的属性都被private了,外界无法直接访问属性,必须对外提供相应的set和get方 ...

  5. DevExpress的下拉框控件ComboxBoxEdit怎样绑定键值对选项

    场景 DevExpress的下拉框控件ComboBoxEdit控件的使用: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1028 ...

  6. C++ 赋值运算符'='的重载(浅拷贝、深拷贝)

    01 赋值运算符重载的需求 有时候希望赋值运算符两边的类型可以不匹配,比如:把一个 int 类型变量赋值给一个Complex(复数)对象,或把一个 char* 类型的字符串赋值给一个字符串对象,此时就 ...

  7. python 通过使用pandas的实现的Excel的批量转换CSV文件的处理

    ---恢复内容开始--- 最近同事在处理文件导入的时候需要把一批文件换成CSV的格式,但是直觉修改后缀是不生效的,而且xlsx和xls的文件没法直接换成CVS的文件,所以找了一下方式,并且自己实现了p ...

  8. Android监视器概述

    还望支持个人博客站:http://www.enjoytoday.cn Android监视器可帮助您分析应用程序的性能,以便您优化,调试和改进它们. 它可以让您从硬件设备或Android模拟器监控应用程 ...

  9. gyp编译工具

    最近用到了 node-gyp 这个工具, 是node 社区对 google gyp 编译工具的一个封装, 使用 node-gyp 工具可以用C++为node 项目编写 addon. 了解了一下 goo ...

  10. TCP/IP协议的分层

    T C P / I P协议族是一组不同的协议组合在一起构成的协议族.尽管通常称该协议族为 T C P / I P,但T C P和I P只是其中的两种协议而已(该协议族的另一个名字是 I n t e r ...