10. Vue的插槽進化及其原理

插槽

在Vue 2.6.0 中,咱們爲具名插槽和做用域插槽引入了一個新的統一的語法(即v-slot指令)。它取代了 slot和slot-scope。vue

匿名和具名插槽slot

父子組件分別以node

<app-layout>另外一個主要段落</app-layout>
複製代碼

數組

<div class="container"><slot></slot></div>
複製代碼

爲例。 父組件生成的render函數爲:bash

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('app-layout',[_v("另外一個主要段落")])],1)}
})
複製代碼

調用:app

// Ctor爲app-layout的子組件構造函數
vnode = createComponent(Ctor, data, context, children, tag);
// 建立Vnode
var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
);
複製代碼

app-layout組件生成的Vnode爲:async

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: ƒ VueComponent(options)
        children: [VNode],
        listeners: undefined,
        propsData: undefined,
        tag: "app-layout"
    },
    context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
    data: {on: undefined, hook: {…}},
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "vue-component-1-app-layout",
    text: undefined,
    child: undefined
}
複製代碼

app-layout的子組件生成的render函數爲:函數

(function anonymous(
) {
with(this){return _c('div',{staticClass:"container"},[_t("default")],2)}
})
複製代碼

this指向app-layout父組件的構造函數。在子組件初始化過程當中的initRender會對slot進行以下處理:ui

vm.$slots = resolveSlots(options._renderChildren, renderContext);
複製代碼

其中options._renderChildren表示一個包含app-layout子組件VNode的數組,具體函數爲this

function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
}
複製代碼

返回{default: [VNode]},此時vm.$slot = {default: [VNode]}vm.$scopedSlots = emptyObject,接下來進入編譯階段,其中的默認插槽slot被genSlot函數解析成spa

_t("default")
複製代碼

在子組件中的_render函數中會格式化插槽的表示

vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);
複製代碼

生成

vm.$scopedSlots = {
  default: ƒ (),
  $hasNormal: true,
  $key: undefined,
  $stable: false
}
複製代碼

接着看_t函數:

// name1爲default, 其餘爲undefined
function renderSlot (
    name,
    fallback,
    props,
    bindObject
  ) {
    var scopedSlotFn = this.$scopedSlots[name];
    var nodes;
    if (scopedSlotFn) { // scoped slot
      props = props || {};
      if (bindObject) {
        if (!isObject(bindObject)) {
          warn(
            'slot v-bind without argument expects an Object',
            this
          );
        }
        props = extend(extend({}, bindObject), props);
      }
      nodes = scopedSlotFn(props) || fallback;
    } else {
        // 從this.$slots中獲取default值,是app-layout組件的text虛擬節點內容[VNode]
        nodes = this.$slots[name] || fallback;
    }

    var target = props && props.slot;
    if (target) {
      return this.$createElement('template', { slot: target }, nodes)
    } else {
      return nodes
    }
}
複製代碼

接着將子組件內容掛載到父級,而後掛載到body上,最後再刪除初始標籤內容。具名插槽同理,只是將default改爲對應的名字xx。

做用域插槽slot-scope

父子組件分別以

<div id="app"><app-layout :items="items"><template slot-scope="list"><div>{{ list.data }}</div></template></app-layout></div>
複製代碼

<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
複製代碼

爲例。 在模板解析template尾標籤時,執行closeElement會對template模板組件會做以下處理:

function processSlotContent (el) {
    if (el.tag === 'template') {
      slotScope = getAndRemoveAttr(el, 'scope');
      /* istanbul ignore if */
      if (slotScope) {
        warn$2(
          "the \"scope\" attribute for scoped slots have been deprecated and " +
          "replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " +
          "can also be used on plain elements in addition to <template> to " +
          "denote scoped slots.",
          el.rawAttrsMap['scope'],
          true
        );
      }
      // 此時 el.slotScope = 'list'
      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
    } 
}
複製代碼

此時會在element元素上掛載: { slotScope: "list" } 接着closeElement函數繼續處理:

if (element.slotScope) {
  // scoped slot
  // keep it in the children list so that v-else(-if) conditions can
  // find it as the prev node.
  var name = element.slotTarget || '"default"'
  ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
currentParent.children.push(element);
element.parent = currentParent;
複製代碼

生成:

{
    scopedSlots: {
        "default": {
            attrsList: []
            attrsMap: {slot-scope: "list"},
            children: [{…}],
            end: 106,
            parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
            plain: false,
            rawAttrsMap: {
                slot-scope: {
                    end: 68,
                    name: "slot-scope",
                    start: 51,
                    value: "list"
                }
            },
            slotScope: "list",
            start: 41,
            tag: "template",
            type: 1
        }
    }
}
複製代碼

生成代碼過程當中通過genScopedSlots, genScopedSlot處理

function genScopedSlot (
    el,
    state
  ) {
    var isLegacySyntax = el.attrsMap['slot-scope'];
    if (el.if && !el.ifProcessed && !isLegacySyntax) {
      return genIf(el, state, genScopedSlot, "null")
    }
    if (el.for && !el.forProcessed) {
      return genFor(el, state, genScopedSlot)
    }
    var slotScope = el.slotScope === emptySlotScopeToken
      ? ""
      : String(el.slotScope);
    var fn = "function(" + slotScope + "){" +
      "return " + (el.tag === 'template'
        ? el.if && isLegacySyntax
          ? ("(" + (el.if) + ")?" + (genChildren(el, state) || 'undefined') + ":undefined")
          : genChildren(el, state) || 'undefined'
        : genElement(el, state)) + "}";
    // reverse proxy v-slot without scope on this.$slots
    var reverseProxy = slotScope ? "" : ",proxy:true";
    return ("{key:" + (el.slotTarget || "\"default\"") + ",fn:" + fn + reverseProxy + "}")
  }
複製代碼

生成

"{ attrs:{"items":items}, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])]}}]),"
複製代碼

完整的render函數爲:

with(this){
    return _c('div',
    {
        attrs:{"id":"app"}
    },
    [
        _c('app-layout',{
            attrs:{"items":items},
            scopedSlots:_u([{key:"default", fn:function(list){
                return [_c('div',[_v(_s(list.data))])]
            }}])
        })
    ] ,1)
}
複製代碼

其中_u函數表示

function resolveScopedSlots (
    fns, // see flow/vnode
    res,
    // the following are added in 2.6
    hasDynamicKeys,
    contentHashKey
  ) {
    res = res || { $stable: !hasDynamicKeys };
    for (var i = 0; i < fns.length; i++) {
      var slot = fns[i];
      if (Array.isArray(slot)) {
        resolveScopedSlots(slot, res, hasDynamicKeys);
      } else if (slot) {
        // marker for reverse proxying v-slot without scope on this.$slots
        if (slot.proxy) {
          slot.fn.proxy = true;
        }
        res[slot.key] = slot.fn;
      }
    }
    if (contentHashKey) {
      (res).$key = contentHashKey;
    }
    return res
}
複製代碼

fns爲[{fn: ƒ (list), key: "default"}],該函數返回

{
    $stable: true,
    default: ƒ (list)
}
複製代碼

app-layout生成的VNode爲:

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: ƒ VueComponent(options),
        children: undefined,
        listeners: undefined,
        propsData: {items: Array(1)},
        tag: "app-layout"
    },
    context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
    data: {
        attrs: {}, 
        scopedSlots: {
            $stable: true,
            default: ƒ (list)
        }, 
        on: undefined, hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
    },
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "vue-component-1-app-layout",
    text: undefined,
    child: undefined
}
複製代碼

layout子組件的render函數爲:

with(this){
    return _c(
        'div',
        [
            _l((items),function(item, index){
                return _t("default", null,{"data": item.text})
            })
        ],
    2)
}
複製代碼

_l函數爲:

function renderList (
    val,
    render
  ) {
    var ret, i, l, keys, key;
    if (Array.isArray(val) || typeof val === 'string') {
      ret = new Array(val.length);
      for (i = 0, l = val.length; i < l; i++) {
        ret[i] = render(val[i], i);
      }
    } else if (typeof val === 'number') {
      ret = new Array(val);
      for (i = 0; i < val; i++) {
        ret[i] = render(i + 1, i);
      }
    } else if (isObject(val)) {
      if (hasSymbol && val[Symbol.iterator]) {
        ret = [];
        var iterator = val[Symbol.iterator]();
        var result = iterator.next();
        while (!result.done) {
          ret.push(render(result.value, ret.length));
          result = iterator.next();
        }
      } else {
        keys = Object.keys(val);
        ret = new Array(keys.length);
        for (i = 0, l = keys.length; i < l; i++) {
          key = keys[i];
          ret[i] = render(val[key], key, i);
        }
      }
    }
    if (!isDef(ret)) {
      ret = [];
    }
    (ret)._isVList = true;
    return ret
}
複製代碼

對數組的每個元素,執行:

return _t("default", null,{"data": item.text})
複製代碼

此過程會觸發getter,添加依賴,_t函數爲:

// name爲default,props爲{data: 'text1'}
function renderSlot (
    name,
    fallback,
    props,
    bindObject
  ) {
      // f函數
    var scopedSlotFn = this.$scopedSlots[name];
    var nodes;
    if (scopedSlotFn) { // scoped slot
      props = props || {};
      if (bindObject) {
        if (!isObject(bindObject)) {
          warn(
            'slot v-bind without argument expects an Object',
            this
          );
        }
        props = extend(extend({}, bindObject), props);
      }
      // 獲取渲染後的[VNode]
      nodes = scopedSlotFn(props) || fallback;
    } else {
      nodes = this.$slots[name] || fallback;
    }

    var target = props && props.slot;
    if (target) {
      return this.$createElement('template', { slot: target }, nodes)
    } else {
      return nodes
    }
}
複製代碼

接着將生成的VNode生成元素節點,並將子組件內容掛載到父級,而後掛載到body上,最後再刪除初始標籤內容。 如今咱們來對比一下普通插槽和做用域插槽的區別:// 普通插槽

slots: {
  xxoo: h('div')
}
// 做用域插槽
scopedSlots: {
  xxoo: (scopedData) => h('div', scopedData.a)
}
複製代碼

做用域插槽和普通插槽的區別在於,子組件拿到它的時候它仍是一個函數,只有你執行該函數,它纔會返回要渲染的內容(即vnode),因此能夠統一爲:

// 普通插槽
slots: {
  xxoo: () => h('div')
}
複製代碼

這個也是Vue2.6.*中的改變之處。

v-slot原理

父子組件分別以

<div id="app"><app-layout :items="items"><template v-slot:default="list"><div>{{ list.data }}</div></template></app-layout></div>
複製代碼

<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
複製代碼

爲例。 父組件在模板解析階段,編譯閉合標籤template時,closeElement函數中會執行processSlotContent,以下:

// 2.6 v-slot syntax
    {
      if (el.tag === 'template') {
        // v-slot on <template>
        // 做用在template上,slotRE = /^v-slot(:|$)|^#/
        var slotBinding = getAndRemoveAttrByRegex(el, slotRE);
        if (slotBinding) {
          var ref = getSlotName(slotBinding);
          var name = ref.name;
          var dynamic = ref.dynamic;
          el.slotTarget = name;
          el.slotTargetDynamic = dynamic;
          el.slotScope = slotBinding.value || emptySlotScopeToken; // force it into a scoped slot for perf
        }
      } else {
        // v-slot on component, denotes default slot
        var slotBinding$1 = getAndRemoveAttrByRegex(el, slotRE);
        if (slotBinding$1) {
          // add the component's children to its default slot var slots = el.scopedSlots || (el.scopedSlots = {}); var ref$1 = getSlotName(slotBinding$1); var name$1 = ref$1.name; var dynamic$1 = ref$1.dynamic; var slotContainer = slots[name$1] = createASTElement('template', [], el); slotContainer.slotTarget = name$1; slotContainer.slotTargetDynamic = dynamic$1; slotContainer.children = el.children.filter(function (c) { if (!c.slotScope) { c.parent = slotContainer; return true } }); slotContainer.slotScope = slotBinding$1.value || emptySlotScopeToken; // remove children as they are returned from scopedSlots now el.children = []; // mark el non-plain so data gets generated el.plain = false; } } } 複製代碼

該函數在element元素中添加屬性:

{
    ...
    slotScope: "list",
    slotTarget: ""default"",
    slotTargetDynamic: false,
    ...
}
複製代碼

而後在app-layout組件的closeElement函數中增長屬性:

...
scopedSlots: {"default": {
    attrsList: [],
    attrsMap: {v-slot:default: "list"},
    children: [{
        attrsList: [],
        attrsMap: {},
        children: [{…}],
        end: 99,
        parent: {type: 1, tag: "template", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},
        plain: true,
        rawAttrsMap: {},
        start: 73,
        tag: "div",
        type: 1}],
    end: 110,
    parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    plain: false,
    rawAttrsMap: {v-slot:default: {
        end: 72,
        name: "v-slot:default",
        start: 51,
        value: "list"
    }},
    slotScope: "list",
    slotTarget: ""default"",
    slotTargetDynamic: false,
    start: 41,
    tag: "template",
    type: 1
}}
...
複製代碼

接着在生成代碼階段,genElement中的genData$2函數會處理上面生成的scopedSlots屬性值:

if (el.scopedSlots) {
    data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
複製代碼

該函數具體爲

function genScopedSlots (
    el,
    slots,
    state
  ) {
    var generatedSlots = Object.keys(slots)
      .map(function (key) { return genScopedSlot(slots[key], state); })
      .join(',');

    return ("scopedSlots:_u([" + generatedSlots + "]" + (needsForceUpdate ? ",null,true" : "") + (!needsForceUpdate && needsKey ? (",null,false," + (hash(generatedSlots))) : "") + ")")
}
複製代碼

genScopedSlot函數的做用是生成以下代碼:

"{key:"default",fn:function(list){return [_c('div',[_v(_s(list.data))])]}}"
複製代碼

返回了

"{ attrs: { "items":items }, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])] } }]),"
複製代碼

生成完整的render函數爲:

with(this){
    return _c('div',
    {attrs:{"id":"app"}},
    [_c('app-layout',{
        attrs:{"items":items},
        scopedSlots:_u([{
            key:"default",
            fn:function(list){return [_c('div',[_v(_s(list.data))])]}}])
        })
    ],1)
}
複製代碼

生成VNode時,_u即resolveScopedSlots函數處理

function resolveScopedSlots (
    fns, // see flow/vnode
    res,
    // the following are added in 2.6
    hasDynamicKeys,
    contentHashKey
  ) {
    res = res || { $stable: !hasDynamicKeys };
    for (var i = 0; i < fns.length; i++) {
      var slot = fns[i];
      if (Array.isArray(slot)) {
        resolveScopedSlots(slot, res, hasDynamicKeys);
      } else if (slot) {
        // marker for reverse proxying v-slot without scope on this.$slots
        if (slot.proxy) {
          slot.fn.proxy = true;
        }
        res[slot.key] = slot.fn;
      }
    }
    if (contentHashKey) {
      (res).$key = contentHashKey;
    }
    return res
}
複製代碼

其中函數參數fns=[{key: "default", fn: ƒ}]res={$stable: true}。返回值爲 {$stable: true, default: ƒ (list)} 生成虛擬DOM節點的函數vnode = createComponent(Ctor, data, context, children, tag),其中data爲

{
    attrs: {},
    hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
    on: undefined,
    scopedSlots: {
        $stable: true,
        default: ƒ (list)
    }
}
複製代碼

app-layout的VNode爲:

{
    syncFactory: undefined,
    asyncMeta: undefined,
    children: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: ƒ VueComponent(options)
        children: undefined,
        listeners: undefined,
        propsData: {},
        tag: "app-layout",
    }
    context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
    data: {attrs: {}, scopedSlots: {
        $stable: true,
        default: ƒ (list)
    }, on: undefined, hook: {…}},
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "vue-component-1-app-layout",
    text: undefined,
    child: undefined
}
複製代碼

子組件與slot-scope一致的render函數爲:

with(this){return _c('div',[
    _l((items),function(item,index){
        return _t("default",null,{"data":item.text})
    })],2)
}
複製代碼

一樣的通過_t函數處理

function renderSlot (
    name,
    fallback,
    props,
    bindObject
  ) {
    var scopedSlotFn = this.$scopedSlots[name];
    var nodes;
    if (scopedSlotFn) { // scoped slot
      props = props || {};
      if (bindObject) {
        if (!isObject(bindObject)) {
          warn(
            'slot v-bind without argument expects an Object',
            this
          );
        }
        props = extend(extend({}, bindObject), props);
      }
      nodes = scopedSlotFn(props) || fallback;
    } else {
      nodes = this.$slots[name] || fallback;
    }

    var target = props && props.slot;
    if (target) {
      return this.$createElement('template', { slot: target }, nodes)
    } else {
      return nodes
    }
}
複製代碼

this.$scopedSlots爲:

{
    default: ƒ (),
    $hasNormal: false,
    $key: undefined,
    $stable: true
}
複製代碼

props爲{data: "text1"},返回的nodes爲子組件的[VNode],最後掛載子組件到父組件,並將父級組件掛載到body頁面,再刪除老的元素節點。此處理過程與slot-scope一致。 匿名插槽模板

<div id="app"><app-layout v-slot:default>另外一個主要段落</app-layout></div>
複製代碼

會被編譯爲:

"_c('app-layout',{ scopedSlots:_u([{key:"default",fn:function(){return [_v("另外一個主要段落")] },proxy:true}]) })"
複製代碼

其餘處理過程一致。

總結

在Vue2.5.*以前,若是是普通插槽就直接訪問的是VNode,而若是是做用域插槽,因爲子組件須要在父組件訪問子組件的數據,因此父組件下是一個未執行的函數(slotScope) => return h('div',slotScope.msg),接受子組件的slotProps參數,在子組件渲染實例時會調用該函數傳入數據。在2.6以後,二者合併,普通插槽也變成一個函數,只是不接受參數了。

相關文章
相關標籤/搜索