深刻理解Vue中的slots/scoped slots

一直對Vue中的slot插槽比較感興趣,下面是本身的一些簡單理解,但願能夠幫助你們更好的理解slot插槽vue

下面結合一個例子,簡單說明slots的工做原理

  1. dx-li子組件的template以下:
<li class="dx-li">
    <slot>
         你好 掘金!
    </slot>
</li>
複製代碼
  1. dx-ul父組件的template以下:
<ul>
    <dx-li>
        hello juejin!
    </dx-li>
</ul>
複製代碼
  1. 結合上述例子以及vue中相關源碼進行分析
    • dx-ul父組件中template編譯後,生成的組件render函數:
    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: []
    }
    複製代碼
    傳遞的插槽內容'hello juejin!'會被編譯成dx-li子組件VNode節點的子節點。
    • 渲染dx-li子組件,其中子組件的render函數:
    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: []
    }
    複製代碼
    初始化dx-li子組件vue實例過程當中,會調用initRender函數:
    function initRender (vm) {
        ...
        // 其中_renderChildren數組,存儲爲 'hello juejin!'的VNode節點;renderContext通常爲父組件Vue實例
        這裏爲dx-ul組件實例
        vm.$slots = resolveSlots(options._renderChildren, renderContext);
        ...
    }
    複製代碼
    其中resolveSlots函數爲:
    /**
     * 主要做用是將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
    }
    複製代碼
    而後掛載dx-li組件時,會調用dx-li組件render函數,在此過程當中會調用renderSlot函數:
    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理解

  1. dx-li子組件的template以下:
<li class="dx-li">	
    <slot str="你好 掘金!">
	    hello juejin!
    </slot>
</li>   
複製代碼
  1. dx-ul父組件的template以下:
<ul>
    <dx-li>
        <span slot-scope="scope">
            {{scope.str}}
        </span>
    </dx-li>
</ul>
複製代碼
  1. 結合例子和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函數:node

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元素的。bash

slots是將父組件編譯生成的插槽VNode,在渲染子組件時,放置到對應子組件渲染VNode樹中。app

scoped slots是將父組件中插槽內容編譯成一個函數,在渲染子組件時,傳入子組件props,生成對應的VNode。最後子組件,根據組件render函數返回VNode節點樹,update渲染真實DOM元素。同時,能夠看出跨組件傳遞插槽也是能夠的,可是必須注意具名插槽傳遞函數

以上是本人對於Slots的一些淺顯理解,關於slot還有不少其餘的知識點。但願能夠幫助你們。因爲本人水平有限,有什麼錯誤和不足,但願指出。ui

相關文章
相關標籤/搜索