Vue之slot深度復制
在Vue中, slot 是一個很有用的特性,可以用來向組件內部插入一些內容。 slot 就是“插槽”的意思,用大白話說就是:定義組件的時候留幾個口子,由用戶來決定插入的內容。
例如我們定義一個組件 MyComponent ,其包含一個 slot :
Vue.component('MyComponent', {
template: `
<div>
<slot></slot>
</div>
`
})
當調用 <MyComponent>123</MyComponent> 時,會渲染為如下DOM結構:
<div>
123
</div>
現在又有新需求了,我們希望調用 <MyComponent>123</MyComponent> 時,渲染出這樣的DOM結構:
<div>
123
123
</div>
看起來很容易實現,即再為 MyComponent 添加一個 slot :
Vue.component('MyComponent', {
template: `
<div>
<slot></slot>
<slot></slot>
</div>
`
})
渲染出的結構也確實如你所愿,唯一美中不足的是控制臺有一個小小的Warning:
Duplicate presence of slot "default" found in the same render tree
如果你不是強迫癥患者,這時候你可以收工安心回家睡覺了。直到有一天你的同事向你抱怨,為什么向 MyComponent 插入一個自定義組件會渲染不出來?
例如有一自定義組件 MyComponent2 :
Vue.component('MyComponent2', {
template: `
<div>456</div>
`
})
當調用 <MyComponent><MyComponent2></MyComponent2></MyComponent> 時,預期渲染為如下DOM結構:
<div>
<div>456</div>
<div>456</div>
</div>
為什么不能正常工作呢?估計是前面的那個Warning搞得鬼,通過查詢發現在Vue 2.0中不允許有重名的 slot :
重名的 Slots 移除
同一模板中的重名 已經棄用。當一個 slot 已經被渲染過了,那么就不能在同一模板其它地方被再次渲染了。如果要在不同位置渲染同一內容,可一用 prop 來傳遞。
文檔中提示可以用 props 來實現,然而在我的用例中顯然是不合適的。經過搜索后,最靠譜的方法是手寫render函數,將 slot 中的內容復制到其他的位置。
將之前的 MyComponent 改為render函數的方式定義:
Vue.component('MyComponent', {
render (createElement) {
return createElement('div', [
...this.$slots.default,
...this.$slots.default
])
}
})
在上面的定義中我們插入了兩個 this.$slots.default ,測試下能不能正常工作。然而并沒有什么卵用,Vue文檔在render函數這一章有以下說明:
VNodes 必須唯一
所有組件樹中的 VNodes 必須唯一
這意味著我們不能簡單地在不同位置引用 this.$slots.default ,必須對 slot 進行深度復制。深度復制的函數如下:
function deepClone(vnodes, createElement) {
function cloneVNode(vnode) {
const clonedChildren= vnode.children && vnode.children.map(vnode => cloneVNode(vnode));
const cloned= createElement(vnode.tag, vnode.data, clonedChildren);
cloned.text= vnode.text;
cloned.isComment= vnode.isComment;
cloned.componentOptions= vnode.componentOptions;
cloned.elm= vnode.elm;
cloned.context= vnode.context;
cloned.ns= vnode.ns;
cloned.isStatic= vnode.isStatic;
cloned.key= vnode.key;
return cloned;
}
const clonedVNodes= vnodes.map(vnode => cloneVNode(vnode))
return clonedVNodes;
}
上面的核心函數就是 cloneVNode() ,它遞歸地創建VNode,實現深度復制。VNode的屬性很多,我并不了解哪些是關鍵屬性,只是參照著Vue的源碼一并地復制過來。
基于以上函數,我們更改 MyComponent 的定義:
Vue.component('MyComponent', {
render (createElement) {
return createElement('div', [
...this.$slots.default,
...deepClone(this.$slots.default, createElement)
])
}
})
經測試,一切正常。
總結
在Vue 1.0中重名的slots并不會出現什么問題,不知道為什么在2.0中取消了這個功能。我聽說React提供了復制Element的標準函數,希望Vue也能提供這個函數,免得大家踩坑。
來自:https://jingsam.github.io/2017/03/08/vnode-deep-clone.html