vue3的runtime-core-实现组件slots功能

这一章来实现插槽,插槽主要分以下几种:默认插槽具名插槽作用域插槽,下面就一步一步实现,可能内容会比较多

slot

默认插槽

首先就是实现默认插槽,新建了个componentSlot文件夹用来测试

App.js

ts 复制代码
export const App = {
    name: 'App',
    render() {
        const app = h('div', {}, 'App')
        const foo = h(Foo, {}, h('p', {}, '123'))
        return h('div', {}, [app, foo])
    },
    setup() {
        return {}
    }
}

Foo.js

可以在这里写上this.$slots,让这个key来返回虚拟节点的children

ts 复制代码
export const Foo = {
    render() {
        const foo = h('p', {}, 'foo')
        console.log(this.$slots);
        return h('div', {}, [foo, this.$slots]) 
    },
    setup() {
        return {}
    }
}

我们希望把app的h函数渲染出来的虚拟节点添加到foo内

实现其实就是获取到foo组件内的vnode的children

还记得之前实现的el吗,专门做了个映射扩展,可以在这里添加上slots

componentPublicInstance.ts

$开头的其实就是给用户提供了一个api

因为之前都是处理好的,所以直接加上就行了

ts 复制代码
const publicPropertiesMap = {
    $el: (i) => i.vnode.el,
    $slots: (i) => i.slots,
}

接下来肯定就是构建我们返回的那个slots,那肯定是在component里添加上预定值

component.ts

之前处理的props的位置其实还有一个slots的TODO,现在就在这实现

ts 复制代码
export function createComponentInstance(vnode) {
    const component = {
        vnode,
        type: vnode.type,
        setupState: {},
        props: {},
        slots: {}, // 预定值
        emit: (event) => { }
    }
    component.emit = emit.bind(null, component)
    return component
}

export function setupComponent(instance) {
    const { vnode: { props, children } } = instance

    initProps(instance, props)
    // 就在这里处理slots,和之前一样,不同功能要分开,我们这里创建一个新文件componentSlots.ts
    initSlots(instance, children) 

    setupStatefulComponent(instance)
}

componentSlots.ts

ts 复制代码
export function initSlots(instance, children) {
    // 实现可以先粗暴一点,直接赋值
    instance.slots = children
}

到这一步在foo.js里是可以打印出来this.$slots的,也是可以渲染出来的

Foo.js

ts 复制代码
render() {
        const foo = h('p', {}, 'foo')
        console.log(this.$slots) // 
        return h('div', {}, [foo, this.$slots])
    },

接下来就做的复杂一些,有些时候我们是可传入一个数组的,这个时候之前的代码肯定是不行的,因为我们去渲染的时候里面必须是一个虚拟节点,不可以是一个数组

App.js

ts 复制代码
export const App = {
    name: 'App',
    render() {
        const app = h('div', {}, 'App')
        const foo = h(Foo, {}, [h('p', {}, '123'), h('p', {}, '567')])
        return h('div', {}, [app, foo])
    },
    setup() {

        return {}
    }
}

Foo.js

ts 复制代码
export const Foo = {
    render() {
        const foo = h('p', {}, 'foo')
        console.log(this.$slots);
        // return h('div', {}, [foo, h('div', {}, this.$slots)])   粗暴解决方式
        //可以封装一个renderSlots函数专门用来处理slots的这个问题
        return h('div', {}, [foo, renderSlots(this.$slots)])  
    },
    setup() {
        return {}
    }
}

我们这里在runtime-core文件夹下专门创建一个helpers文件夹,里面专门存放解决专用问题的函数

renderSlots.ts

ts 复制代码
export function renderSlots(slots) {
    return createVNode('div', {}, slots)
}

创建完之后,我们把这个导出去,在runtime-core的index.ts里导出

runtime-core/index.ts

ts 复制代码
export { renderSlots } from "./helpers/renderSlots"

回到app.js问题来了,那就是我们现在使用之前的单个节点的时候还能不能正常使用呢,答案是肯定不行的,我们这里需要做两个支持,一个是单个节点,一个是多个节点,所以这里需要再专门处理一下,我们处理这种问题一般都是回到init环节,直接打开initSlots函数内

componentSlots.ts

既然我们数组可以使用,那单个节点可以用数组包裹一下就行了

ts 复制代码
export function initSlots(instance, children) {
    // 外面做了处理数组的情况,里面做一下处理
    instance.slots = Array.isArray(children) ? children : [children]
}
具名插槽

现在单值和数组都支持了,那我们就可以让我们的需求再次升级,传入了两个节点,那我是不是可以指定渲染位置,这就是新的需求

kotlin 复制代码
// 比如我想以一个在前、一个在后的形式渲染
return h('div', {}, [前,foo, 后])  

这里其实只需要满足两点就可以完成这个需求

1.获取到要渲染的元素

2.获取到要渲染的位置

那么我们之前使用的数据结构是数组,现在换成对象,用key就可以精确的获取到要渲染的元素

App.js

数据结构改成这样,就可以用key来获取到要渲染的元素

ts 复制代码
export const App = {
    name: 'App',
    render() {
        const app = h('div', {}, 'App')
        const foo = h(Foo, {}, {
            header: h('p', {}, 'header'),
            footer: h('p', {}, 'footer')
        })
        return h('div', {}, [app, foo])
    },
    setup() {

        return {}
    }
}

Foo.js

ts 复制代码
export const Foo = {
    render() {
        const foo = h('p', {}, 'foo')
        console.log(this.$slots);
        // 可以给一个指定的 key 用来获取要渲染的元素,当前是要渲染的位置
        return h('div', {}, [renderSlots(this.$slots, 'header'), foo, renderSlots(this.$slots, 'footer')])
    },
    setup() {
        return {}
    }
}

转战到要扩展的 renderSlots函数

renderSlots.ts

这里需要获取要渲染的元素,还需要判断一下

ts 复制代码
export function renderSlots(slots, name) {
    const slot = slots[name]
    if (slot) {
        return createVNode('div', {}, slot)
    }
}

之前我们在initslots的时候把数据结构改成了数组,现在肯定要对象

initSlots.ts

ts 复制代码
export function initSlots(instance, children) {
    const slots = {}
    for (const key in children) {
        const value = children[key]
        // slot
        // 和之前要做一样的处理,判断是不是数组
        slots[key] = Array.isArray(value) ? value : [value]
    }

    // 那我们处理好的slots要赋值给instance的slots
    instance.slots = slots
}

处理完之后就可以跑起来看看了,结果就是通过了

那么initSlots的处理环节太多了,我们可以把有些逻辑代码都抽离出去,重构一下,提高可读性以及细节语义化

initSlots.ts

ts 复制代码
export function initSlots(instance, children) {
    // 不需要赋值了,直接把slots的引用给到它
    normalizeObjectSlots(children, instance.slots)
}

function normalizeObjectSlots(children, slots) {
    for (const key in children) {
        const value = children[key]
        // slot
        // 和之前要做一样的处理,判断是不是数组
        slots[key] = normalizeSlotValue(value)
    }
}

function normalizeSlotValue(value) {
    return Array.isArray(value) ? value : [value]
}

我们在重构完一定要记得重新跑一遍,看看重构有没有问题,要先打包哦

完成以上的两个需求点之后呢,这个具名插槽就完成了,接下来就可以继续实现,比如说'作用域插槽'

作用域插槽

啥意思呢,其实就是foo组件内部的变量传出去,让app组件能够获取到

Foo.js

参数可能不止一个,所以我们用对象的方式

ts 复制代码
export const Foo = {
    render() {
        const foo = h('p', {}, 'foo')
        const age = 18
        const name = 'xin'
        console.log(this.$slots);                          // { age }包裹起来
        return h('div', {}, [renderSlots(this.$slots, 'header', {age}), foo, renderSlots(this.$slots, 'footer', {name})])
    },
    setup() {
        return {}
    }
}

我们直接在App.js内部是没办法直接拿到的,其实可以改造一下,用函数传参的形式把变量传过来

App.js

ts 复制代码
export const App = {
    name: 'App',
    render() {
        const app = h('div', {}, 'App')
        const foo = h(Foo, {}, {
            // 传入的时候是对象包裹起来的,那接受参数的时候肯定要用解构的方式拿出来
            header: ({age}) => h('p', {}, 'header'+ age),
            footer: ({name}) => h('p', {}, 'footer' + name)
        })
        return h('div', {}, [app, foo])
    },
    setup() {
        return {}
    }
}

renderSlots.ts

ts 复制代码
export function renderSlots(slots, name, props) {
    const slot = slots[name]
    if (slot) {
        // 现在的slot是一个function了
        if(typeof slot === 'function') {
            return createVNode('div', {}, slot(props))
        }
    }
}

componentSlots.ts

之前我们可以直接得到它的值,来判断是不是数组,现在需要调用得到返回值才行,所以调用一下,参数呢,同样是函数传参形式拿到

ts 复制代码
export function initSlots(instance, children) {
    // 不需要赋值了,直接把slots的引用给到它
    normalizeObjectSlots(children, instance.slots)
}

function normalizeObjectSlots(children, slots) {
    for (const key in children) {
        const value = children[key]
        // slot
        slots[key] = (props) => normalizeSlotValue(value(props))
    }
}

function normalizeSlotValue(value) {
    // 和之前要做一样的处理,判断是不是数组
    return Array.isArray(value) ? value : [value]
}

重新跑起来也是没有任何问题的

这样我们就基本完成了作用域插槽,下一步就是看看有没有什么地方是需要重构或者要优化的点

首先就是类型判断,不是所有的节点都会有children的,或者说不是有对应的slots类型,我们就可以给当前的虚拟节点进行一个类型判断,加上一个flag,就像之前一样加上flag的类型

vnode.ts

那怎么判定它是不是slots children呢,这里是有两个点需要约束的,第一:它必须是一个组件类型,第二:它的children必须是一个object类型

ts 复制代码
export function createVNode(type, props?, children?) {
    const vnode = {
        type,
        props,
        children,
        shapeFlag: getShapeFlag(type),
        el: null
    }
    // children
    if (typeof children === 'string') {
        // 等同于 vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN
        vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
    }
    else if (Array.isArray(children)) {
        vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
    }

    // 必须是个组件且children是个对象才进行ShapeFlags更改
    // 下面去添加一下ShapeFlags.SLOT_CHILDREN
    if((vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && isObject(children)) {
        vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN
    }

    return vnode
}

ShapeFlags.ts

ts 复制代码
export const enum ShapeFlags {
    ELEMENT = 1,                   
    STATEFUL_COMPONENT = 1 << 1,    
    TEXT_CHILDREN = 1 << 2,       
    ARRAY_CHILDREN = 1 << 3,        
    SLOT_CHILDREN = 1 << 4
}

接下来就在initSlots里去做一下判断

componentSlots.ts

ts 复制代码
export function initSlots(instance, children) {
    if (instance.vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
        // 不需要赋值了,直接把slots的引用给到它
        normalizeObjectSlots(children, instance.slots)
    }
}

都搞完了之后记得重新跑一下,结果也是没有任何问题

那么其实各位可以去一个网站对比一下自己写的和vue3实际实现有什么出入,或者看看模板编译出来是啥样

vue-next-template-explorer.netlify.app

template-explorer.vuejs.org/

哪个能用,就用哪个

最后我们实现的是和vue3的实现是一致的,各位可以去试试

结语

这章其实是很长的,各位看到这里也是不容易的,这章讲的很详细,比之前讲的要详细的多,所以内容也自然呈现的更多,各位看官觉着不错,不妨一键三连

相关推荐
web1508541593532 分钟前
vue 集成 webrtc-streamer 播放视频流 - 解决阿里云内外网访问视频流问题
vue.js·阿里云·webrtc
一个处女座的程序猿O(∩_∩)O3 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
迷糊的『迷』3 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886353 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app
陈大爷(有低保)3 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js
北京_宏哥4 小时前
python接口自动化(四十)- logger 日志 - 下(超详解)
python·前端框架·自动化运维
cronaldo914 小时前
研发效能DevOps: Vite 使用 Element Plus
vue.js·vue·devops
CoderLiu4 小时前
用Rust写了一个css插件,sass从此再见了
前端·javascript·前端框架
百罹鸟4 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
Java_慈祥4 小时前
慈様や 前端学习导航👩🏻‍🚀🚀
前端·javascript·vue.js