由繁到简: Vue slot 插槽解析过程

334 阅读4分钟

前言

插槽的出现让组件之间的通信不局限于使用ref和prop, 插槽不止可以传递DOM元素也可以传递作用域属性,而且还能插入指定标记位置,让封装组件变得更加的灵活

插槽的使用

插槽的API里面有些变动,slot-scope已经被标明被废弃到了,因此我们这里讨论的是v-slot属性,下面我们就简单的介绍一下插槽的简单用法

1.匿名插槽

匿名插槽没有作用域,没有别名识别,会把父组件中子组件children标签里面包裹的全部DOM元素替换到子组件里面的标签里面

 //父组件
    <div id="app">       
         <children>            
            <p>传递给插槽的内容1</p>            
            <p>传递给插槽的内容2</p>        
        </children>    
    </div>

 //子组件
    <div>
       <slot/>
    </div>

2.别名插槽

别名插槽的好处就是不是全部东西都一步梭哈到子类里面,通过别名的方式更佳的灵活指定需要插入的内容以及位置,它可以让内容自定义化,大大提高的组件封装的灵活度

//父组件
    <div id="app">        
        <children>            
            <div>匿名内容1</div>            
            <div>匿名内容2</div>            
            <template v-slot:header>                
                <p>头部</p>            
            </template>            
            <template v-slot:footer>                
                <p>尾部</p>            
            </template>        
        </children>    
    </div>
//子组件    
    <div>        
        <slot name="header"/>         
        <slot name="footer"/>         
        <slot/>    
    </div>

3.作用域插槽

普通插槽就是只能传递DOM元素,但是实际的需求中往往是多样化了,作用域插槽就是在原有普通插槽的基础上进行了数据传输,插槽=》将父组件编写的DOM内容插入到子组件,作用域插槽=》在子组件可以对父组件的插槽内容进行数据的传递

    //父组件    
<div id="app">        
  <children>            
    <div slot-scope="scope">                
      <p>{{ scope.data }}</p>            
    </div> 
    <template v-slot:header="scope">                
      <p>{{ scope. data }}</p>            
    </template>       
  </children>    
</div>

//子组件    
<div>        
  <p>hello world</p>        
  <slot data="插槽传值" />        
  <slot name="header" data="别名插槽传值" />    
</div>

插槽的实现

我们知道,Vue里面所写的html最终都会被解析成vnode, 要想知道插槽怎么实现,就要知道组件解析成vnode是怎么样的数据格式,下面就开始使用图文的方式来说明插槽是怎么实现的

按照解析原则,上面的父组件和子组件的DOM元素解析成vnode后就上图所示那样子了,

这下子就有了一种一目了然的情况了,因为插槽的内容是在父组件编写的,但是它最终渲染的位置是在子组件,这时候我们只需要把componentsOptions里面的children的所有vnodo子类替换到子组件标签名为"slot"位置的vnode即可实现了,而作用域组件和别名组件的只是基础组件的附加内容,每个vnode里面的data属性就是存放当前DOM的attributes属性,把别名和作用域的属性值存放到data属性即可实现传值和位置识别

说是如此简单,那Vue源码里面是怎么替换的呢?

Vue的每个组件都会调用其$mount方法将template的DOM元素解析然后生成真实DOM

而我们也可以自主编写一个render函数来实现template的解析,比如上面的子组件如果采用自主编写render函数,那么它是长这样

//子组件
<div>
  <p>我是子组件</p>
  <slot/>
</div>

//render函数
$createElement(
    'div',
    {}, 
    [ 
        $createElement('p',{},['我是子组件'])
        _t('default') 
    ]
)

$createElement函数内部如何实现的呢?移步到我的写的上一个文章

Vue的组件是如何生成的?

那这里的_t函数的怎么回事呢?其实它就是一个把tag = 'childrent'组件里面的componentsOptions.children里面的所有vnode内容塞到当前位置

在Vue内部它长这样

export function renderSlot (  
  name: string, //插槽别名,匿名插槽默认分配名字为default   
  fallback: ?Array<VNode>,  props: ?Object, //作用域插槽传值参数  
  bindObject: ?Object): ?Array<VNode> {  
   //得到子组件中<slot/>插槽对应在父组件的插槽内容,this代表当前组件对象
  //这里得到的值是一个包装过的函数,执行后就返回了vnode+  
  const scopedSlotFn = this.$scopedSlots[name]  
  let nodes  
  if (scopedSlotFn) { 
    // scoped slot    
    props = props || {}    
    if (bindObject) {       
     props = extend(extend({}, bindObject), props)    
    }    
    //得到需要插入的vnode+    
    nodes = scopedSlotFn(props) || fallback  
  } else {    
    nodes = this.$slots[name] || fallback  
  }  
  const target = props && props.slot  
  if (target) {    
    return this.$createElement('template', { slot: target }, nodes)  
  } else {    
    return nodes  
  }
}

里面并不复杂,其实就是从当前组件实例 this.scopedSlots\[name\]得到插槽的内容,然后将其内容return出去,那this.scopedSlots里面的内容是什么时候被挂载插槽内容进去的呢?移步到Vue.prototype._render函数里面:

Vue.prototype._render = function (): VNode {    
  const vm: Component = this    
  const { render, _parentVnode } = vm.$options    
  if (_parentVnode) {      
    vm.$scopedSlots = normalizeScopedSlots( 
      _parentVnode.data.scopedSlots,       
      vm.$slots,         
      vm.$scopedSlots      
    )
}

normalizeScopedSlots函数就是将插槽的vnode用一个函数包装起来,normalizeScopedSlots的第一和第三个参数不必里面,我们之间看vm.slots的内容,它是保存了插槽的vnode,那slots的内容,它是保存了插槽的vnode,那slots属性在哪里挂载的呢?打开Vue.prototype.initRender

export function initRender (vm: Component) {  
vm._vnode = null 
vm._staticTrees = null 
const options = vm.$options  
......
vm.$slots = resolveSlots(options._renderChildren, renderContext)

它的值就存在了options._renderChildren里面,那这个属性在哪来挂载的呢?

我们知道new Vue的时候会调用了其this.init(options)方法,然后首先的操作就是调用megeOptions组件实现配置项的合并,但是根组件和子组件不一样,子组件调用的是initInternalComponent方法进行配置项的合并,而initInternalComponent方法里面就将_renderChildren指向了父组件中解析的children标签对应的vnode 

{     
   tag: 'children',        
   data: { },        
   componentsOptions: {             
     children: [{                
       tag: 'p',                
       data: {},                
       children: [{                    
         text: '我是插槽内容'                
       }]            
     }]         
   }        
   children: []
}

这样子走了一波三折的路线,其实总结起来就是:父组件template调用render函数解析成vnode,然后调用update方法将vnode生成真实DOM,在update过程中发现了vnode里面有组件类型children,那么就通过Vue.extend创建一个子组件children,然后传递vnode进去保存父子关系,这样子组件children即可获取父组件传递的vnode里面所有的内容了