因为平时在公司写代码业务不是很通用,除了一些使用 elementUI 的 B 端项目,用 slot 的机会比较少。前段时间阿里被面试(没想找工作 o(╥﹏╥)o)问到了这个问题,写个文章稍微研究一下。我当时的回答是:没看过源码,应该是基于 Vnode 类似的渲染逻辑解析的
源码来自 vue 2 版本的 vue-dev 分支的 2.6.12
源码定位
先在 src 文件下搜索 slot,东西很杂,不太好定位,只好去 src/core/intance/render-helpers/render-slot.js
看起,这里面就一个函数
export function renderSlot(
name: string,
fallback: ?Array,
props: ?Object,
bindObject: ?Object
): ?Array {
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) { // scoped slot
props = props || {}
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !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
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
从里面大致可以看住,这函数的功能是:返回插入的 nodes 节点 / 在目标模板上插入这个 nodes 节点
render-helpers 目录下的文件从名字上就能理解是辅助 render 的,这些函数会通过 src/core/instance/render-helpers/index.js
绑在 Vue.FunctionalRenderContext 上
export function installRenderHelpers (target: any) {
...
target._t = renderSlot
...
}
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
installRenderHelpers(FunctionalRenderContext.prototype)
搜 ._t 的调用是搜不到的,因为这里的 _t 函数还有其他的函数,是用于 render 函数中使用的,举个例子:cli 自带 demo 的 helloWorld 组件搞一个插槽(父组件插入 123),他的 render 函数返回值长这个德行:return _c("div", { staticClass: "hello" }, [_vm._t("default")], 2)
,父组件的长这个德行return _c( "div", { attrs: { id: "app" } }, [_c("HelloWorld", [_vm._v("123")])], 1)
,其中_c 代表 createElement、 _t 代表 renderSlot、 _v 代表 createTextVNode
具体实现
子组件的 slot
子组件是怎么知道这有个 slot 的?其实就是 template 解析或者 render 函数解析,把
父组件的 slot 内容
父组件怎么知道子组件有东西接着 slot?其实父组件并不怎么关心子组件是不是有插槽,子组件作为父组件的节点,父组件只需要把插槽里面的内容当做子组件的 children 传进去就可以了
父子组件 slot 内容的传递
所以这个问题就变成了,父组件传进来的内容是怎么赋值到子组件的 $slots 上的
Vue.prototype._init = function (options?: Object) {
if (options && options._isComponent) {
initInternalComponent(vm, options)
}
...
initRender(vm);
...
}
function initInternalComponent(vm: Component, options: InternalComponentOptions) {
...
const parentVnode = options._parentVnode
...
const vnodeComponentOptions = parentVnode.componentOptions
...
opts._renderChildren = vnodeComponentOptions.children
...
}
function initRender(vm) {
...
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}
initInternalComponent 会把 _renderChildren 挂在 options 上
这里的 options._renderChildren 就是 上面提到的 [_vm._v("123")]
对应的 [Vnode]
function resolveSlots(
children: ?Array,
context: ?Component
): { [key: string]: Array } {
if (!children || !children.length) {
return {}
}
const slots = {}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const 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
) {
const name = data.slot
const 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 (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
这里就是把 _renderChildren 变成 slots,这里还有点 匿名和具名插槽
的内容:也就是 slot 的 name 是有值还是默认的 default。。。
完~