Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解

Posted greatdesert

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解相关的知识,希望对你有一定的参考价值。

普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的。

有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单元格数据的显示格式,我们可以通过作用域插槽自定义数据的显示格式,对于二次开发来说具有很强的扩展性。

作用域插槽使用<template>来定义模板,可以带两个参数,分别是:

     slot-scope    ;模板里的变量,旧版使用scope属性

     slot              ;该作用域插槽的name,指定多个作用域插槽时用到,默认为default,即默认插槽

例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
          <Child>
            <template slot="header" slot-scope="props">                 <!--定义了名为header的作用域插槽的模板-->
              <h1>props.info.name-props.info.age</h1>
            </template>
            <template slot-scope="show">                                <!--定义了默认作用域插槽的模板-->
              <p>show.today</p>
            </template>
          </Child>
      </div>
      <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        Vue.component(Child,
          template:`<div class="container">
                        <header><slot name="header" :info="info"></slot></header>     //header插槽
                        <main><slot today="礼拜一">默认内容</slot></main>               //默认插槽
                    </div>`,
              data()
                return  info:name:ge,age:25 
              
        )
        debugger
        new Vue(
          el: #app,
          data:
            title:我是标题,
            msg:我是内容
          
        )
      </script>
</body>
</html>

我们在子组件定义了两个插槽,如下:

     header插槽内通过v-bind绑定了一个名为info的特性,值为一个对象,包含一个name和age属性

     另一个是普通插槽,传递了一个today特性,值为礼拜一

父组件引用子组件时定义了模板,渲染后结果如下:

技术图片

对应的html代码如下:

技术图片

其实Vue内部把父组件template下的子节点编译成了一个函数,在子组件实例化时调用的,所以作用域才是子组件的作用域

 

 源码分析


 父组件解析模板将模板转换成AST对象时会执行processSlot()函数,如下:

function processSlot (el)        //第9767行   解析slot插槽
  if (el.tag === ‘slot‘)           //如果是slot
    /*普通插槽的逻辑*/
   else 
    var slotScope;
    if (el.tag === ‘template‘)                 //如果标签名为template(作用域插槽的逻辑)
      slotScope = getAndRemoveAttr(el, ‘scope‘);          //尝试获取scope
      /* istanbul ignore if */  
      if ("development" !== ‘production‘ && slotScope)   //在开发环境下报一些信息,因为scope属性已淘汰,新版本开始用slot-scope属性了
        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.",
          true
        );
      
      el.slotScope = slotScope || getAndRemoveAttr(el, ‘slot-scope‘); //获取slot-scope特性,值保存到AST对象的slotScope属性里
     else if ((slotScope = getAndRemoveAttr(el, ‘slot-scope‘))) 
      /*其它分支*/
    
    var slotTarget = getBindingAttr(el, ‘slot‘);          //尝试获取slot特性
    if (slotTarget)                                      //如果获取到了
      el.slotTarget = slotTarget === ‘""‘ ? ‘"default"‘ : slotTarget;   //则保存到el.slotTarget里面
      // preserve slot as an attribute for native shadow DOM compat
      // only for non-scoped slots.
      if (el.tag !== ‘template‘ && !el.slotScope) 
        addAttr(el, ‘slot‘, slotTarget);
      
    
  

执行到这里,对于<template slot="header" slot-scope="props"> 节点来说,添加了一个slotScope和slotTarget属性,如下:

 技术图片

对于<template slot-scope="show">节点来说,由于没有定义slot属性,它的AST对象如下:

技术图片

作用域插槽和普通节点最大的不同点是它不会将当前结点挂在AST对象树上,而是挂在了父节点的scopedSlots属性上。

在解析完节点属性后会执行start()函数内的末尾会判断如果发现AST对象.slotScope存在,则会在currentParent对象(也就是父AST对象)的scopedSlots上新增一个el.slotTarget属性,值为当前template对应的AST对象。 

if (currentParent && !element.forbidden)     //第9223行  解析模板时的逻辑 如果当前对象不是根对象, 且不是style和text/javascript类型script标签
  if (element.elseif || element.else)            //如果有elseif或else指令存在(设置了v-else或v-elseif指令)
    processIfConditions(element, currentParent);
   else if (element.slotScope)  // scoped slot  //如果存在slotScope属性,即是作用域插槽
    currentParent.plain = false;
    var name = element.slotTarget || ‘"default"‘;(currentParent.scopedSlots || (currentParent.scopedSlots = ))[name] = element;   //给父元素增加一个scopedSlots属性,值为数组,每个键名为对应的目标名称,值为对应的作用域插槽AST对象
   else  
    currentParent.children.push(element);
    element.parent = currentParent;
  

这样父节点就存在一个slotTarget属性了,值为对应的作用域插槽AST对象,例子里执行到这一步对应slotTarget如下:

技术图片

default和header分别对应父组件里的两个template节点

父组件执行generate的时候,如果AST对象的scopedSlots属性存在,则执行genScopedSlots()函数拼凑data:

  if (el.scopedSlots)      //如果el.scopedSlots存在,即子节点存在作用域插槽  
    data += (genScopedSlots(el.scopedSlots, state)) + ",";    //调用genScopedSlots()函数,并拼接到data里面
   

genScopedSlots函数会返回scopedSlots:_u([])函数字符串,_u就是全局的resolveScopedSlots函数,genScopedSlots如下:

function genScopedSlots (     //第10390行 
  slots,
  state
) 
  return ("scopedSlots:_u([" + (Object.keys(slots).map(function (key)      //拼凑一个_u字符串
      return genScopedSlot(key, slots[key], state)                            //遍历slots,执行genScopedSlot,将返回值保存为一个数组,作为_u的参数
    ).join(‘,‘)) + "])")

genScopedSlot会拼凑每个slots,如下:

function genScopedSlot ( //第10399行 
  key,
  el,
  state
) 
  if (el.for && !el.forProcessed) 
    return genForScopedSlot(key, el, state)
  
  var fn = "function(" + (String(el.slotScope)) + ")" +      //拼凑一个函数,el.slotScope就是模板里设置的slot-scope属性
    "return " + (el.tag === ‘template‘
      ? el.if
        ? ((el.if) + "?" + (genChildren(el, state) || ‘undefined‘) + ":undefined")
        : genChildren(el, state) || ‘undefined‘
      : genElement(el, state)) + "";
  return ("key:" + key + ",fn:" + fn + "")

解析后生成的render函数如下:

with(this)return _c(‘div‘,attrs:"id":"app",[_c(‘child‘,scopedSlots:_u([key:"header",fn:function(props)return [_c(‘h1‘,[_v(_s(props.info.name)+"-"+_s(props.info.age))])],key:"default",fn:function(show)return [_c(‘p‘,[_v(_s(show.today))])]]))],1)

这样看着不清楚,我们整理一下,如下:

with(this) 
    return _c(
        ‘div‘, 
        attrs: "id": "app",
        [_c(‘child‘, 
            scopedSlots: _u([
              key: "header",fn: function(props) return [_c(‘h1‘, [_v(_s(props.info.name) + "-" + _s(props.info.age))])],
              key: "default",fn: function(show) return [_c(‘p‘, [_v(_s(show.today))])]
            ])
          
        )], 
        1)

可以看到_u的参数是一个对象,键名为插槽名,值是一个函数,最后子组件会执行这个函数的,创建子组件的实例时,会将scopedSlots属性保存到data.scopedSlots上

对于子组件的编译过程和普通插槽没有什么区别,唯一不同的是会有attr属性,例子里的组件编译后生成的render函数如下:

with(this)return _c(‘div‘,staticClass:"container",[_c(‘header‘,[_t("header",null,info:info)],2),_v(" "),_c(‘main‘,[_t("default",[_v("默认内容")],today:"礼拜一")],2)])

这样看着也不清楚,我们整理一下,如下:

with(this) 
    return _c(‘div‘, staticClass: "container",
        [
          _c(‘header‘, [_t("header", null, info: info)], 2), 
          _v(" "), 
          _c(‘main‘, [_t("default", [_v("默认内容")], today: "礼拜一")], 2)
        ]
      )

可以看到最后和普通插槽一样也是执行_t函数的,不过在_t函数内会优先从scopedSlots中获取模板,如下:

function renderSlot (       //渲染插槽
  name,
  fallback,
  props,
  bindObject
) 
  var scopedSlotFn = this.$scopedSlots[name];           //尝试从 this.$scopedSlots中获取名为name的函数,也就是我们在上面父组件渲染生成的render函数里的作用域插槽相关函数
  var nodes;
  if (scopedSlotFn)  // scoped slot                    //如果scopedSlotFn存在
    props = props || ;
    if (bindObject) 
      if ("development" !== ‘production‘ && !isObject(bindObject)) 
        warn(
          ‘slot v-bind without argument expects an Object‘,
          this
        );
      
      props = extend(extend(, bindObject), props);
    
    nodes = scopedSlotFn(props) || fallback;          //最后执行scopedSlotFn这个函数,参数为props,也就是特性数组
   else 
    /*普通插槽的分支*/
  

  var target = props && props.slot;
  if (target) 
    return this.$createElement(‘template‘,  slot: target , nodes)
   else 
    return nodes
  

最后将nodes返回,也就是在父节点的template内定义的子节点返回,作为最后渲染的节点集合。

以上是关于Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解的主要内容,如果未能解决你的问题,请参考以下文章

Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解

Vue.js 源码分析(二十二) 指令篇 v-model指令详解

Vue.js 源码分析(二十) 指令篇 v-once指令详解

MATLAB可视化实战系列(二十六)-MATLAB非线性可视化之线性系统相图(附源码)

Vue.js 源码分析—— Slots 是如何实现的

vue.js methods中的方法互相调用时变量的作用域是怎样的