这一章来实现插槽,插槽主要分以下几种:默认插槽 ,具名插槽 ,作用域插槽,下面就一步一步实现,可能内容会比较多
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
哪个能用,就用哪个
最后我们实现的是和vue3的实现是一致的,各位可以去试试
结语
这章其实是很长的,各位看到这里也是不容易的,这章讲的很详细,比之前讲的要详细的多,所以内容也自然呈现的更多,各位看官觉着不错,不妨一键三连