Vue Slot 的实现方式(源码~)

2020-09-17  本文已影响0人  zpkzpk

因为平时在公司写代码业务不是很通用,除了一些使用 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<VNode>,
    props: ?Object,
    bindObject: ?Object
): ?Array<VNode> {
    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> 变成了一个_t(renderSlot) 函数,_t 函数会自己去 $slots 里面取插槽的内容

父组件的 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<VNode>,
    context: ?Component
): { [key: string]: Array<VNode> } {
    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。。。

完~

上一篇下一篇

猜你喜欢

热点阅读