深入理解Vue中的slots/scoped slots

逃跑的兔子發表於2018-04-01

一直對Vue中的slot插槽比較感興趣,下面是自己的一些簡單理解,希望可以幫助大家更好的理解slot插槽

下面結合一個例子,簡單說明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函式:

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還有很多其他的知識點。希望可以幫助大家。由於本人水平有限,有什麼錯誤和不足,希望指出。

相關文章