普通的插槽里面的数据是在父组件里定义的,而做用域插槽里的数据是在子组件定义的。javascript
有时候做用域插槽颇有用,好比使用Element-ui表格自定义模板时就用到了做用域插槽,Element-ui定义了每一个单元格数据的显示格式,咱们能够经过做用域插槽自定义数据的显示格式,对于二次开发来讲具备很强的扩展性。html
做用域插槽使用<template>来定义模板,能够带两个参数,分别是:vue
slot-scope ;模板里的变量,旧版使用scope属性java
slot ;该做用域插槽的name,指定多个做用域插槽时用到,默认为default,即默认插槽node
例如:npm
<!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"> <Child> <template slot="header" slot-scope="props"> <!--定义了名为header的做用域插槽的模板--> <h1>{{props.info.name}}-{{props.info.age}}</h1> </template> <template slot-scope="show"> <!--定义了默认做用域插槽的模板--> <p>{{show.today}}</p> </template> </Child> </div> <script> Vue.config.productionTip=false; Vue.config.devtools=false; Vue.component('Child',{ template:`<div class="container"> <header><slot name="header" :info="info"></slot></header> //header插槽 <main><slot today="礼拜一">默认内容</slot></main> //默认插槽 </div>`, data(){ return { info:{name:'ge',age:25} } } }) debugger new Vue({ el: '#app', data:{ title:'我是标题', msg:'我是内容' } }) </script> </body> </html>
咱们在子组件定义了两个插槽,以下:数组
header插槽内经过v-bind绑定了一个名为info的特性,值为一个对象,包含一个name和age属性app
另外一个是普通插槽,传递了一个today特性,值为礼拜一函数
父组件引用子组件时定义了模板,渲染后结果以下:源码分析
对应的html代码以下:
其实Vue内部把父组件template下的子节点编译成了一个函数,在子组件实例化时调用的,因此做用域才是子组件的做用域
源码分析
父组件解析模板将模板转换成AST对象时会执行processSlot()函数,以下:
function processSlot (el) { //第9767行 解析slot插槽 if (el.tag === 'slot') { //若是是slot /*普通插槽的逻辑*/ } else { var slotScope; if (el.tag === 'template') { //若是标签名为template(做用域插槽的逻辑) slotScope = getAndRemoveAttr(el, 'scope'); //尝试获取scope /* istanbul ignore if */ if ("development" !== 'production' && slotScope) { //在开发环境下报一些信息,由于scope属性已淘汰,新版本开始用slot-scope属性了 warn$2( "the \"scope\" attribute for scoped slots have been deprecated and " + "replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " + "can also be used on plain elements in addition to <template> to " + "denote scoped slots.", true ); } el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope'); //获取slot-scope特性,值保存到AST对象的slotScope属性里 } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) { /*其它分支*/ } var slotTarget = getBindingAttr(el, 'slot'); //尝试获取slot特性 if (slotTarget) { //若是获取到了 el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget; //则保存到el.slotTarget里面 // preserve slot as an attribute for native shadow DOM compat // only for non-scoped slots. if (el.tag !== 'template' && !el.slotScope) { addAttr(el, 'slot', slotTarget); } } } }
执行到这里,对于<template slot="header" slot-scope="props"> 节点来讲,添加了一个slotScope和slotTarget属性,以下:
对于<template slot-scope="show">节点来讲,因为没有定义slot属性,它的AST对象以下:
做用域插槽和普通节点最大的不一样点是它不会将当前结点挂在AST对象树上,而是挂在了父节点的scopedSlots属性上。
在解析完节点属性后会执行start()函数内的末尾会判断若是发现AST对象.slotScope存在,则会在currentParent对象(也就是父AST对象)的scopedSlots上新增一个el.slotTarget属性,值为当前template对应的AST对象。
if (currentParent && !element.forbidden) { //第9223行 解析模板时的逻辑 若是当前对象不是根对象, 且不是style和text/javascript类型script标签 if (element.elseif || element.else) { //若是有elseif或else指令存在(设置了v-else或v-elseif指令) processIfConditions(element, currentParent); } else if (element.slotScope) { // scoped slot //若是存在slotScope属性,便是做用域插槽 currentParent.plain = false; var name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; //给父元素增长一个scopedSlots属性,值为数组,每一个键名为对应的目标名称,值为对应的做用域插槽AST对象 } else { currentParent.children.push(element); element.parent = currentParent; } }
这样父节点就存在一个slotTarget属性了,值为对应的做用域插槽AST对象,例子里执行到这一步对应slotTarget以下:
default和header分别对应父组件里的两个template节点
父组件执行generate的时候,若是AST对象的scopedSlots属性存在,则执行genScopedSlots()函数拼凑data:
if (el.scopedSlots) { //若是el.scopedSlots存在,即子节点存在做用域插槽 data += (genScopedSlots(el.scopedSlots, state)) + ","; //调用genScopedSlots()函数,并拼接到data里面 }
genScopedSlots函数会返回scopedSlots:_u([])函数字符串,_u就是全局的resolveScopedSlots函数,genScopedSlots以下:
function genScopedSlots ( //第10390行 slots, state ) { return ("scopedSlots:_u([" + (Object.keys(slots).map(function (key) { //拼凑一个_u字符串 return genScopedSlot(key, slots[key], state) //遍历slots,执行genScopedSlot,将返回值保存为一个数组,做为_u的参数 }).join(',')) + "])") }
genScopedSlot会拼凑每一个slots,以下:
function genScopedSlot ( //第10399行 key, el, state ) { if (el.for && !el.forProcessed) { return genForScopedSlot(key, el, state) } var fn = "function(" + (String(el.slotScope)) + "){" + //拼凑一个函数,el.slotScope就是模板里设置的slot-scope属性 "return " + (el.tag === 'template' ? el.if ? ((el.if) + "?" + (genChildren(el, state) || 'undefined') + ":undefined") : genChildren(el, state) || 'undefined' : genElement(el, state)) + "}"; return ("{key:" + key + ",fn:" + fn + "}") }
解析后生成的render函数以下:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"header",fn:function(props){return [_c('h1',[_v(_s(props.info.name)+"-"+_s(props.info.age))])]}},{key:"default",fn:function(show){return [_c('p',[_v(_s(show.today))])]}}])})],1)}
这样看着不清楚,咱们整理一下,以下:
with(this) { return _c( 'div', {attrs: {"id": "app"}}, [_c('child', { scopedSlots: _u([ {key: "header",fn: function(props) {return [_c('h1', [_v(_s(props.info.name) + "-" + _s(props.info.age))])]}}, {key: "default",fn: function(show) {return [_c('p', [_v(_s(show.today))])]}} ]) } )], 1) }
能够看到_u的参数是一个对象,键名为插槽名,值是一个函数,最后子组件会执行这个函数的,建立子组件的实例时,会将scopedSlots属性保存到data.scopedSlots上
对于子组件的编译过程和普通插槽没有什么区别,惟一不一样的是会有attr属性,例子里的组件编译后生成的render函数以下:
with(this){return _c('div',{staticClass:"container"},[_c('header',[_t("header",null,{info:info})],2),_v(" "),_c('main',[_t("default",[_v("默认内容")],{today:"礼拜一"})],2)])}
这样看着也不清楚,咱们整理一下,以下:
with(this) { return _c('div', {staticClass: "container"}, [ _c('header', [_t("header", null, {info: info})], 2), _v(" "), _c('main', [_t("default", [_v("默认内容")], {today: "礼拜一"})], 2) ] ) }
能够看到最后和普通插槽同样也是执行_t函数的,不过在_t函数内会优先从scopedSlots中获取模板,以下:
function renderSlot ( //渲染插槽 name, fallback, props, bindObject ) { var scopedSlotFn = this.$scopedSlots[name]; //尝试从 this.$scopedSlots中获取名为name的函数,也就是咱们在上面父组件渲染生成的render函数里的做用域插槽相关函数 var nodes; if (scopedSlotFn) { // scoped slot //若是scopedSlotFn存在 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; //最后执行scopedSlotFn这个函数,参数为props,也就是特性数组 } else { /*普通插槽的分支*/ } var target = props && props.slot; if (target) { return this.$createElement('template', { slot: target }, nodes) } else { return nodes } }
最后将nodes返回,也就是在父节点的template内定义的子节点返回,做为最后渲染的节点集合。