一直對Vue中的slot插槽比較感興趣,下面是自己的一些簡單理解,希望可以幫助大家更好的理解slot插槽
下面結合一個例子,簡單說明slots的工作原理
- dx-li子元件的template如下:
<li class="dx-li">
<slot>
你好 掘金!
</slot>
</li>
複製程式碼
- dx-ul父元件的template如下:
<ul>
<dx-li>
hello juejin!
</dx-li>
</ul>
複製程式碼
- 結合上述例子以及vue中相關原始碼進行分析
- dx-ul父元件中template編譯後,生成的元件render函式:
傳遞的插槽內容'hello juejin!'會被編譯成dx-li子元件VNode節點的子節點。module.exports={ render:function (){ var _vm=this; var _h=_vm.$createElement; var _c=_vm._self._c||_h; // 其中_vm.v為createTextVNode建立文字VNode的函式 return _c('ul', [_c('dx-li', [_vm._v("hello juejin!")])], 1) }, staticRenderFns: [] } 複製程式碼
- 渲染dx-li子元件,其中子元件的render函式:
初始化dx-li子元件vue例項過程中,會呼叫initRender函式:module.exports={ render:function (){ var _vm=this; var _h=_vm.$createElement; var _c=_vm._self._c||_h; // 其中_vm._v 函式為renderSlot函式 return _c('li', {staticClass: "dx-li" }, [_vm._t("default", [_vm._v("你好 掘金!")])], 2 ) }, staticRenderFns: [] } 複製程式碼
其中resolveSlots函式為:function initRender (vm) { ... // 其中_renderChildren陣列,儲存為 'hello juejin!'的VNode節點;renderContext一般為父元件Vue例項 這裡為dx-ul元件例項 vm.$slots = resolveSlots(options._renderChildren, renderContext); ... } 複製程式碼
然後掛載dx-li元件時,會呼叫dx-li元件render函式,在此過程中會呼叫renderSlot函式:/** * 主要作用是將children VNodes轉化成一個slots物件. */ export function resolveSlots ( children: ?Array<VNode>, context: ?Component ): { [key: string]: Array<VNode> } { const slots = {} // 判斷是否有children,即是否有插槽VNode if (!children) { return slots } // 遍歷父元件節點的孩子節點 for (let i = 0, l = children.length; i < l; i++) { const child = children[i] // data為VNodeData,儲存父元件傳遞到子元件的props以及attrs等 const data = child.data /* 移除slot屬性 * <span slot="abc"></span> * 編譯成span的VNode節點data = {attrs:{slot: "abc"}, slot: "abc"},所以這裡刪除該節點attrs的slot */ if (data && data.attrs && data.attrs.slot) { delete data.attrs.slot } /* 判斷是否為具名插槽,如果為具名插槽,還需要子元件/函式子元件渲染上下文一致。主要作用: *當需要向子元件的子元件傳遞具名插槽時,不會保持插槽的名字。 * 舉個栗子: * child元件template: * <div> * <div class="default"><slot></slot></div> * <div class="named"><slot name="foo"></slot></div> * </div> * parent元件template: * <child><slot name="foo"></slot></child> * main元件template: * <parent><span slot="foo">foo</span></parent> * 此時main渲染的結果: * <div> * <div class="default"><span slot="foo">foo</span></div> <div class="named"></div> * </div> */ if ((child.context === context || child.fnContext === context) && data && data.slot != null ) { const name = data.slot const slot = (slots[name] || (slots[name] = [])) // 這裡處理父元件採用template形式的插槽 if (child.tag === 'template') { slot.push.apply(slot, child.children || []) } else { slot.push(child) } } else { // 返回匿名default插槽VNode陣列 (slots.default || (slots.default = [])).push(child) } } // 忽略僅僅包含whitespace的插槽 for (const name in slots) { if (slots[name].every(isWhitespace)) { delete slots[name] } } return slots } 複製程式碼
export function renderSlot ( name: string, // 子元件中slot的name,匿名default fallback: ?Array<VNode>, // 子元件插槽中預設內容VNode陣列,如果沒有插槽內容,則顯示該內容 props: ?Object, // 子元件傳遞到插槽的props bindObject: ?Object // 針對<slot v-bind="obj"></slot> obj必須是一個物件 ): ?Array<VNode> { // 判斷父元件是否傳遞作用域插槽 const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { // scoped slot props = props || {} if (bindObject) { if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) { warn( 'slot v-bind without argument expects an Object', this ) } props = extend(extend({}, bindObject), props) } // 傳入props生成相應的VNode nodes = scopedSlotFn(props) || fallback } else { // 如果父元件沒有傳遞作用域插槽 const slotNodes = this.$slots[name] // warn duplicate slot usage if (slotNodes) { if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) { warn( `Duplicate presence of slot "${name}" found in the same render tree ` + `- this will likely cause render errors.`, this ) } // 設定父元件傳遞插槽的VNode._rendered,用於後面判斷是否有重名slot slotNodes._rendered = true } // 如果沒有傳入插槽,則為預設插槽內容VNode nodes = slotNodes || fallback } // 如果還需要向子元件的子元件傳遞slot /*舉個栗子: * Bar元件: <div class="bar"><slot name="foo"/></div> * Foo元件:<div class="foo"><bar><slot slot="foo"/></bar></div> * main元件:<div><foo>hello</foo></div> * 最終渲染:<div class="foo"><div class="bar">hello</div></div> */ const target = props && props.slot if (target) { return this.$createElement('template', { slot: target }, nodes) } else { return nodes } } 複製程式碼
scoped slots理解
- dx-li子元件的template如下:
<li class="dx-li">
<slot str="你好 掘金!">
hello juejin!
</slot>
</li>
複製程式碼
- dx-ul父元件的template如下:
<ul>
<dx-li>
<span slot-scope="scope">
{{scope.str}}
</span>
</dx-li>
</ul>
複製程式碼
- 結合例子和Vue原始碼簡單作用域插槽
- dx-ul父元件中template編譯後,產生元件render函式:
module.exports={
render:function (){
var _vm=this;
var _h=_vm.$createElement;
var _c=_vm._self._c||_h;
return _c('ul', [_c('dx-li', {
// 可以編譯生成一個物件陣列
scopedSlots: _vm._u([{
key: "default",
fn: function(scope) {
return _c('span',
{},
[_vm._v(_vm._s(scope.str))]
)
}
}])
})], 1)
},
staticRenderFns: []
}
複製程式碼
其中 _vm._u函式:
function resolveScopedSlots (
fns, // 為一個物件陣列,見上文scopedSlots
res
) {
res = res || {};
for (var i = 0; i < fns.length; i++) {
if (Array.isArray(fns[i])) {
// 遞迴呼叫
resolveScopedSlots(fns[i], res);
} else {
res[fns[i].key] = fns[i].fn;
}
}
return res
}
複製程式碼
子元件的後續渲染過程與slots類似。scoped slots原理與slots基本是一致,不同的是編譯父元件模板時,會生成一個返回結果為VNode的函式。當子元件匹配到父元件傳遞作用域插槽函式時,呼叫該函式生成對應VNode。
總結
其實slots/scoped slots 原理是非常簡單的,我們只需明白一點vue在渲染元件時,是根據VNode渲染實際DOM元素的。
slots是將父元件編譯生成的插槽VNode,在渲染子元件時,放置到對應子元件渲染VNode樹中。
scoped slots是將父元件中插槽內容編譯成一個函式,在渲染子元件時,傳入子元件props,生成對應的VNode。最後子元件,根據元件render函式返回VNode節點樹,update渲染真實DOM元素。同時,可以看出跨元件傳遞插槽也是可以的,但是必須注意具名插槽傳遞。
以上是本人對於Slots的一些淺顯理解,關於slot還有很多其他的知識點。希望可以幫助大家。由於本人水平有限,有什麼錯誤和不足,希望指出。