前言
插槽的出现让组件之间的通信不局限于使用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函数内部如何实现的呢?移步到我的写的上一个文章
那这里的_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属性在哪里挂载的呢?打开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里面所有的内容了