Vue.js 源码分析(二十五) 高级应用 插槽 详解
我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过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 源码分析(二十五) 高级应用 插槽 详解的更多相关文章
- Vue.js 源码分析(二十) 指令篇 v-once指令详解
数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值,例如:<p>Message: {{ msg }}</p>以后每当msg属性发生了改变,插值处的内 ...
- Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解
普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的. 有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单 ...
- Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解
对于过度动画如果要同时渲染整个列表时,可以使用transition-group组件. transition-group组件的props和transition组件类似,不同点是transition-gr ...
- Vue.js 源码分析(二十八) 高级应用 transition组件 详解
transition组件可以给任何元素和组件添加进入/离开过渡,但只能给单个组件实行过渡效果(多个元素可以用transition-group组件,下一节再讲),调用该内置组件时,可以传入如下特性: n ...
- Vue.js 源码分析(二十四) 高级应用 自定义指令详解
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上 ...
- Vue.js 源码分析(二十三) 指令篇 v-show指令详解
v-show的作用是将表达式值转换为布尔值,根据该布尔值的真假来显示/隐藏切换元素,它是通过切换元素的display这个css属性值来实现的,例如: <!DOCTYPE html> < ...
- Vue.js 源码分析(二十一) 指令篇 v-pre指令详解
该指令会跳过所在元素和它的子元素的编译过程,也就是把这个节点及其子节点当作一个静态节点来处理,例如: <!DOCTYPE html> <html lang="en" ...
- Vue.js 源码分析(二十二) 指令篇 v-model指令详解
Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...
- Vue.js 源码分析(十三) 基础篇 组件 props属性详解
父组件通过props属性向子组件传递数据,定义组件的时候可以定义一个props属性,值可以是一个字符串数组或一个对象. 例如: <!DOCTYPE html> <html lang= ...
随机推荐
- keras EfficientNet介绍,在ImageNet任务上涨点明显 | keras efficientnet introduction
本文首发于个人博客https://kezunlin.me/post/88fbc049/,欢迎阅读最新内容! keras efficientnet introduction Guide About Ef ...
- 知识图谱辅助金融领域NLP任务
从人工智能学科诞生之初起,自然语言处理(NLP)就是人工智能核心的研究问题之一.NLP的重要性是毋庸置疑的,它能够实现以自然语言交流为特征的高级人机交互,使机器能“阅读”所有以文字形式记录的人类知识, ...
- Spring Cloud Gateway-自定义异常处理
前提 我们平时在用SpringMVC的时候,只要是经过DispatcherServlet处理的请求,可以通过@ControllerAdvice和@ExceptionHandler自定义不同类型异常的处 ...
- wpf file embeded resource is readonly,Copy always will copy the file and its folder to the bin folder
Wpf file embeded resource will compile the file into the assembly and it will be readonly and can no ...
- 【OOM】解决思路
一.什么是OOM? OOM就是outOfMemory,内存溢出!可能是每一个java人员都能遇到的问题!原因是堆中有太多的存活对象(GC-ROOT可达),占满了堆空间. 二.怎么解决? 1.拿到内存溢 ...
- 清新水墨色中国风通用教育培训课件PPT模板
模板来源:http://ppt.dede58.com/jiaoxuekejian/26220.html
- Redis中使用redis-cli及密码登录
使用redis-cli登录后如果Redis中设置了密码那么输入密码可能会出现: NOAUTH Authentication required的错. 这个时候可以输入:auth password 进行登 ...
- vue 地图可视化 maptalks 篇
Maptalks 项目是一个 HTML5 的地图引擎, 基于原生 ES6 Javascript 开发: - 二三维一体化地图, 通过二维地图的旋转 /倾斜增加三维视角 - 插件化设计, 能与其他图形库 ...
- C语言、指针(一)
指针(一) “带*类型” 的特征探测:宽度 “带*类型” 的特征探测:声明 “带*类型” 的特征探测:赋值 “带*类型” 的特征探测:++ -- “带*类型” 的特征探测:加上/减去 一个整数 “带* ...
- Python对文件的读写操作
Python使用open函数来读写文件,open函数的第一个参数是文件名,第二个参数是可选的,有4种常见模式:(1)r 打开一个文件来读数据,这是默认模式:(2)w 打开一个文件来写数据,如果文件已有 ...