比如下面这种写法
<template> <div> <shop><div>金拱门</div></shop> </div> </template>
金拱门三个字是显示不出来的,显示的会是shop组件的template的内容。但是我们用element-ui组件的时候,会发现很多地方是能直接显示出来的,比如el-button按钮上的文字,那么element是怎样做到的呢?
当然Vue提供了slot节点,这是第一种方式,但是element有的地方用了下面说的第二种方式,其实两种方式原理都一样,先说element的这第二种方式:
在上面的例子里,当前组件在创建真实节点的时候,组件的vm会根据template生成render函数从而生成虚拟节点,生成虚拟节点的时候,是没有普通节点和组件节点的区别的,div和shop在render函数眼里是一样的,div是根节点,子节点数组(children属性)是[shop的虚拟节点],同样的shop的虚拟节点的子节点数组是金拱门这个div。
一般情况下,shop节点会根据自己的template生成render函数,进而生成虚拟节点、真实节点,插入父节点的相应位置,如果现在想用内部的<div>金拱门</div>来代替,那么就要在生成虚拟节点的时候用可以生成金拱门的虚拟节点替换template自己的虚拟节点。
vm生成虚拟节点的时候 render函数>template>el的html,所以考虑在vm中重写render函数,比如下面的
render(f) {
return f('div', this.$slots.default)
}
这里的f参数的意义可以看vm._update方法中vm._render方法的源码,vnode = render.call(vm._renderProxy, vm.$createElement); 第一个参数是调用者,可以看作vm,第二个参数其实大致上就是vm._c。所以 vnode = render()相当于vnode = ()=>return createElement(vm, a, b, c, d, true)。而这里的a是tag, b是data,但是如果b传进去一个数组,这个数组会赋值给c,也就是children属性。到这里一切都通了,除了:
this.$slots.default是啥?
Vue源码6161行patch方法(这时候是在父组件的watcher的update方法中调用patch方法),在第一次创建周期的时候是从6208行的createElm一路createChildren这样找到shop节点,调用createElm来创建shop节点,因为child是个组件节点,所以进入5627行的createComponent方法,在5678行调用4213行的init方法(这个是挂载在data上的组件节点专用的初始化方法,和Vue方法中的_init方法不一样),生成shop的VueComponent对象vm,生成vm的时候,vm._init方法中有initRender方法
里面有
vm.$slots = resolveSlots(options._renderChildren, renderContext);
这句话,简单理解就是把options._renderChildren数组中的元素放入vm.$slots.default数组中
那么_renderChildren又是什么?在_init方法中,initRender之前有个initInternalComponent方法,在里面发现是parentVnode.componentOptions的children属性,parentVnode就是shop对应的vNode,
那么vnode中的componentOptions的children属性在哪?那要去vnode生成的时候的_c方法中去看,4586行的_c一直点下去,4517行,createComponent
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
);
对比VNode的构造函数,发现componentOptions就是{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }这个对象,children: children,这个children是createComponent的参数,
其实就是父组件生成shop节点时候的的children属性。
再来回过头看第一种方法:slot节点写在template中或者是el挂载的节点的子节点(不会是render函数的tag,这样的话直接生成<slot>的html节点,浏览器不认识会忽略)
上面两种方法无论哪一种,都要经过$mount被重写的在vue.js最后的那一段逻辑,生成render函数也就是生成虚拟节点,这个过程中遇到tag为slot的时候会特殊处理:
compileToFunctions--->createCompiler--->var code = generate(ast, options);--->else if (el.tag === 'slot') {return genSlot(el, state)}--->在genSlot中,res = "_t(" + slotName + (children ? ("," + children) : '');,下面就是如果slot有属性,处理属性,处理属性这里也很重要,但是先不说,只看_t这是个啥,_t=renderSlot,点进去看,这里如果slot节点没有被赋值name属性,那么name就是default,先看最简单的return nodes = this.$slots[name] || fallback;又遇到大熟人this.$slots.default了,这个是包含slot节点的组件节点在父节点中的子虚拟节点数组,一切又都能说通了。
但是还有一个问题是
||fallback这个是啥?fallback是renderSlot方法的第二个参数,在genSlot方法中,是children=genChildren,简单理解的话是包含slot那个组件节点的template中,slot节点内部的元素,比如下面的
<shop><slot>肯德基</<slot></shop>肯德基这个text元素,所以,如果<shop>节点在父节点中有子节点数组,那么会在shop的template包含的slot节点替换成children数组,优先显示,如果没有,那么会显示
原来就在slot中的元素。