我們定義一個組件的時候,可以在組件的某個節點內預留一個位置,當父組件調用該組件的時候可以指定該位置具體的內容,這就是插槽的用法,子組件模板可以通過slot標簽(插槽)規定對應的內容放置在哪裡,比如: 渲染結果為: 對應的html節點如下: 引用AppLayout這個組件時,我們指定了header和f ...
我們定義一個組件的時候,可以在組件的某個節點內預留一個位置,當父組件調用該組件的時候可以指定該位置具體的內容,這就是插槽的用法,子組件模板可以通過slot標簽(插槽)規定對應的內容放置在哪裡,比如:
<!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"> <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 }
例子里的子組件執行完後$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源碼了,還好平時有在做筆記,很快就理解了,不管什麼框架,後端也是的,語言其實不難,難的是理解框架的設計思想,從事程式員這一行因為要學的東西很多,我們也不可能每個去記住的,所以筆記很重要。