这一章来实现插槽,插槽主要分以下几种:默认插槽,具名插槽,作用域插槽,下面就一步一步实现,可能内容会比较多
slot
默认插槽
首先就是实现默认插槽,新建了个componentSlot文件夹用来测试
App.js
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 {}}}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 {} } }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
export const Foo = {render() {const foo = h('p', {}, 'foo')console.log(this.$slots);return h('div', {}, [foo, this.$slots])},setup() {return {}}}export const Foo = { render() { const foo = h('p', {}, 'foo') console.log(this.$slots); return h('div', {}, [foo, this.$slots]) }, setup() { return {} } }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
因为之前都是处理好的,所以直接加上就行了
const publicPropertiesMap = {$el: (i) => i.vnode.el,$slots: (i) => i.slots,}const publicPropertiesMap = { $el: (i) => i.vnode.el, $slots: (i) => i.slots, }const publicPropertiesMap = { $el: (i) => i.vnode.el, $slots: (i) => i.slots, }
接下来肯定就是构建我们返回的那个slots,那肯定是在component里添加上预定值
component.ts
之前处理的props的位置其实还有一个slots的TODO,现在就在这实现
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 } } = instanceinitProps(instance, props)// 就在这里处理slots,和之前一样,不同功能要分开,我们这里创建一个新文件componentSlots.tsinitSlots(instance, children)setupStatefulComponent(instance)}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) }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
export function initSlots(instance, children) {// 实现可以先粗暴一点,直接赋值instance.slots = children}export function initSlots(instance, children) { // 实现可以先粗暴一点,直接赋值 instance.slots = children }export function initSlots(instance, children) { // 实现可以先粗暴一点,直接赋值 instance.slots = children }
到这一步在foo.js里是可以打印出来this.$slots的,也是可以渲染出来的
Foo.js
render() {const foo = h('p', {}, 'foo')console.log(this.$slots) //return h('div', {}, [foo, this.$slots])},render() { const foo = h('p', {}, 'foo') console.log(this.$slots) // return h('div', {}, [foo, this.$slots]) },render() { const foo = h('p', {}, 'foo') console.log(this.$slots) // return h('div', {}, [foo, this.$slots]) },
接下来就做的复杂一些,有些时候我们是可传入一个数组的,这个时候之前的代码肯定是不行的,因为我们去渲染的时候里面必须是一个虚拟节点,不可以是一个数组
App.js
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 {}}}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 {} } }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
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 {}}}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 {} } }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
export function renderSlots(slots) {return createVNode('div', {}, slots)}export function renderSlots(slots) { return createVNode('div', {}, slots) }export function renderSlots(slots) { return createVNode('div', {}, slots) }
创建完之后,我们把这个导出去,在runtime-core的index.ts里导出
runtime-core/index.ts
export { renderSlots } from "./helpers/renderSlots"export { renderSlots } from "./helpers/renderSlots"export { renderSlots } from "./helpers/renderSlots"
回到app.js问题来了,那就是我们现在使用之前的单个节点的时候还能不能正常使用呢,答案是肯定不行的,我们这里需要做两个支持,一个是单个节点,一个是多个节点,所以这里需要再专门处理一下,我们处理这种问题一般都是回到init环节,直接打开initSlots函数内
componentSlots.ts
既然我们数组可以使用,那单个节点可以用数组包裹一下就行了
export function initSlots(instance, children) {// 外面做了处理数组的情况,里面做一下处理instance.slots = Array.isArray(children) ? children : [children]}export function initSlots(instance, children) { // 外面做了处理数组的情况,里面做一下处理 instance.slots = Array.isArray(children) ? children : [children] }export function initSlots(instance, children) { // 外面做了处理数组的情况,里面做一下处理 instance.slots = Array.isArray(children) ? children : [children] }
具名插槽
现在单值和数组都支持了,那我们就可以让我们的需求再次升级,传入了两个节点,那我是不是可以指定渲染位置,这就是新的需求
// 比如我想以一个在前、一个在后的形式渲染return h('div', {}, [前,foo, 后])// 比如我想以一个在前、一个在后的形式渲染 return h('div', {}, [前,foo, 后])// 比如我想以一个在前、一个在后的形式渲染 return h('div', {}, [前,foo, 后])
这里其实只需要满足两点就可以完成这个需求
1.获取到要渲染的元素
2.获取到要渲染的位置
那么我们之前使用的数据结构是数组,现在换成对象,用key就可以精确的获取到要渲染的元素
App.js
数据结构改成这样,就可以用key来获取到要渲染的元素
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 {}}}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 {} } }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
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 {}}}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 {} } }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
这里需要获取要渲染的元素,还需要判断一下
export function renderSlots(slots, name) {const slot = slots[name]if (slot) {return createVNode('div', {}, slot)}}export function renderSlots(slots, name) { const slot = slots[name] if (slot) { return createVNode('div', {}, slot) } }export function renderSlots(slots, name) { const slot = slots[name] if (slot) { return createVNode('div', {}, slot) } }
之前我们在initslots的时候把数据结构改成了数组,现在肯定要对象
initSlots.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的slotsinstance.slots = slots}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 }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
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]}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] }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
参数可能不止一个,所以我们用对象的方式
export const Foo = {render() {const foo = h('p', {}, 'foo')const age = 18const name = 'xin'console.log(this.$slots); // { age }包裹起来return h('div', {}, [renderSlots(this.$slots, 'header', {age}), foo, renderSlots(this.$slots, 'footer', {name})])},setup() {return {}}}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 {} } }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
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 {}}}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 {} } }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
export function renderSlots(slots, name, props) {const slot = slots[name]if (slot) {// 现在的slot是一个function了if(typeof slot === 'function') {return createVNode('div', {}, slot(props))}}}export function renderSlots(slots, name, props) { const slot = slots[name] if (slot) { // 现在的slot是一个function了 if(typeof slot === 'function') { return createVNode('div', {}, slot(props)) } } }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
之前我们可以直接得到它的值,来判断是不是数组,现在需要调用得到返回值才行,所以调用一下,参数呢,同样是函数传参形式拿到
export function initSlots(instance, children) {// 不需要赋值了,直接把slots的引用给到它normalizeObjectSlots(children, instance.slots)}function normalizeObjectSlots(children, slots) {for (const key in children) {const value = children[key]// slotslots[key] = (props) => normalizeSlotValue(value(props))}}function normalizeSlotValue(value) {// 和之前要做一样的处理,判断是不是数组return Array.isArray(value) ? value : [value]}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] }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类型
export function createVNode(type, props?, children?) {const vnode = {type,props,children,shapeFlag: getShapeFlag(type),el: null}// childrenif (typeof children === 'string') {// 等同于 vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDRENvnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN}else if (Array.isArray(children)) {vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN}// 必须是个组件且children是个对象才进行ShapeFlags更改// 下面去添加一下ShapeFlags.SLOT_CHILDRENif((vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && isObject(children)) {vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN}return vnode}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 }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
export const enum ShapeFlags {ELEMENT = 1,STATEFUL_COMPONENT = 1 << 1,TEXT_CHILDREN = 1 << 2,ARRAY_CHILDREN = 1 << 3,SLOT_CHILDREN = 1 << 4}export const enum ShapeFlags { ELEMENT = 1, STATEFUL_COMPONENT = 1 << 1, TEXT_CHILDREN = 1 << 2, ARRAY_CHILDREN = 1 << 3, SLOT_CHILDREN = 1 << 4 }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
export function initSlots(instance, children) {if (instance.vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {// 不需要赋值了,直接把slots的引用给到它normalizeObjectSlots(children, instance.slots)}}export function initSlots(instance, children) { if (instance.vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { // 不需要赋值了,直接把slots的引用给到它 normalizeObjectSlots(children, instance.slots) } }export function initSlots(instance, children) { if (instance.vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { // 不需要赋值了,直接把slots的引用给到它 normalizeObjectSlots(children, instance.slots) } }
都搞完了之后记得重新跑一下,结果也是没有任何问题
那么其实各位可以去一个网站对比一下自己写的和vue3实际实现有什么出入,或者看看模板编译出来是啥样
vue-next-template-explorer.netlify.app
哪个能用,就用哪个
最后我们实现的是和vue3的实现是一致的,各位可以去试试
结语
这章其实是很长的,各位看到这里也是不容易的,这章讲的很详细,比之前讲的要详细的多,所以内容也自然呈现的更多,各位看官觉着不错,不妨一键三连