Vue之slot深度復制

cgaa8818 8年前發布 | 11K 次閱讀 Vue.js Vue.js開發

在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

 

 本文由用戶 cgaa8818 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!