普通的插槽裡面的數據是在父組件里定義的,而作用域插槽里的數據是在子組件定義的。 有時候作用域插槽很有用,比如使用Element-ui表格自定義模板時就用到了作用域插槽,Element-ui定義了每個單元格數據的顯示格式,我們可以通過作用域插槽自定義數據的顯示格式,對於二次開發來說具有很強的擴展性。 ...
普通的插槽裡面的數據是在父組件里定義的,而作用域插槽里的數據是在子組件定義的。
有時候作用域插槽很有用,比如使用Element-ui表格自定義模板時就用到了作用域插槽,Element-ui定義了每個單元格數據的顯示格式,我們可以通過作用域插槽自定義數據的顯示格式,對於二次開發來說具有很強的擴展性。
作用域插槽使用<template>來定義模板,可以帶兩個參數,分別是:
slot-scope ;模板里的變數,舊版使用scope屬性
slot ;該作用域插槽的name,指定多個作用域插槽時用到,預設為default,即預設插槽
例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/[email protected]/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屬性
另一個是普通插槽,傳遞了一個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內定義的子節點返回,作為最後渲染的節點集合。