美文网首页
Vue Slot 的实现方式(源码~)

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

作者: zpkzpk | 来源:发表于2020-09-17 15:11 被阅读0次

    因为平时在公司写代码业务不是很通用,除了一些使用 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。。。

    完~

    相关文章

      网友评论

          本文标题:Vue Slot 的实现方式(源码~)

          本文链接:https://www.haomeiwen.com/subject/pouhyktx.html