slot实现
原理解析
template中的内容最终会被compile
成render函数
,render函数内部会调用h函数
转换成vnode
使用slot的地方是this.slots,内部有配置的插槽额名称,如果使用者没有传递配置,则是通过default进行默认值配置的;
组件的插槽指组件会预留一个槽位,该槽位具体要渲染的内容由用户插入;
实现组件的默认插槽
- 功能点包括
- 在组件的
render
函数中通过this.$slots
获取到父组件传入的默认插槽的内容,并在子组件对应的默认插槽中渲染出来 - 支持插槽传入数组
- 在组件的
- 案例场景搭建
// App.js
import { h, createTextVNode } from "../../lib/guide-mini-vue.esm.js";
import { Foo } from "./Foo.js";
export const App = {
setup() {
return {};
},
render() {
const foo = h(Foo, {}, h("p", {}, "default slot"));
const foo1 = h(Foo, {}, h("p", {}, {
header:h("p",{},"header"),
footer:h("p",{},"footer")
}));
const foo2 = h("h1", {}, "normal slot");
return h("div", {}, [foo,foo1, foo2]);
},
};
// Foo.js
import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js";
export const Foo = {
setup() {
return {};
},
render() {
const foo = h("p", {}, "foo");
const foo2 = h("h2", {}, "foo2");
return h("div", {}, [foo, foo2]);
},
};
- 渲染结果
实现逻辑
-
添加组件公共属性 -
$slots
- 实现思路:在子组件
Foo
中可以通过this.$slots
获取到父组件传入的slots
插槽的内容,然后放入到h
函数中进行渲染
// Foo.js import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js"; export const Foo = { setup() { return {}; }, render() { console.log(this.$slots,'this.$slots=========') const foo = h("p", {}, "foo"); const foo2 = h("h2", {}, "foo2"); // 获取到Foo组件中的虚拟节点.vnode children->this.$slots 然后将其渲染出来 // 方式一:将传入的数组结构转换成一个个vnode 不建议 简单粗暴 return h("div", {}, [foo, foo2, this.$slots]); }, };
- 存在的问题:需要将
slots
以属性的方式挂载到组件实例上,然后可以在组件公共实例中通过$slots
获取到
// src/runtime-core/component.ts // 备注:内部调用逻辑 render -> patch -> processComponent(挂载组件) -> mountComponent(通过虚拟节点创建组件实例) -> createComponentInstance(初始化组件实例属性配置) export function createComponentInstance(vnode,parent) { const component = { vnode, type: vnode.type, props: {}, slots: {}, isMounted: false, // 标识是否是初次加载还是后续依赖数据的更新操作 subTree: null, emit: ()=>{}, provides:parent?parent.provides:{}, parent, render: vnode.render, setupState: {}, }; component.emit = emit.bind(null,component) as any return component; }
// src/runtime-core/componentPublicInstance.ts import { hasOwn } from "../shared/index"; const publicPropertiesMap = { $el: (i) => i.vnode.el, $slots: (i) => i.slots }; export const PublicInstanceProxyHandlers = { get({ _: instance }, key) { const { setupState,props } = instance; // if (key in setupState) { // return setupState[key]; // } if(hasOwn(setupState,key)){ return setupState[key] } else if(hasOwn(props,key)){ return props[key] } // if(key === '$el'){ // return instance.vnode.el // } const publicGetter = publicPropertiesMap[key]; if (publicGetter) { return publicGetter(instance); } }, };
- 初始化Slots - initSlots
- 此时
slots
就是vnode.children
,挂载需要对组件实例instance
进行slots
挂载
- 此时
// src/runtime-core/componentSlots.ts export function initSlots(instance, children) { instance.slots = children; }
// src/runtime-core/component.ts export function setupComponent(instance) { // 处理setup的信息 初始化props 初始化Slots等 initProps(instance,instance.vnode.props), initSlots(instance,instance.vnode.children), setupStatefulComponent(instance); }
- 此时打印
this.$slots
结果是
// Foo.js import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js"; export const Foo = { setup() { return {}; }, render() { console.log(this.$slots,'this.$slots=========') const foo = h("p", {}, "foo"); const foo2 = h("h2", {}, "foo2"); // 获取到Foo组件中的虚拟节点.vnode children->this.$slots 然后将其渲染出来 // 方式一:将传入的数组结构转换成一个个vnode 不建议 简单粗暴 return h("div", {}, [foo, foo2, this.$slots]); }, };
- 实现思路:在子组件
插入数组到插槽中
-
内部逻辑
- 当前实现了在子组件插槽中插入一个
Element
类型的vnode
,但实际上应该支持多个vnode
的渲染 - 存在问题:此时
this.$slots
是个数组类型,数组内部的元素是vnode
,而在上述的实现中,子组件是直接通过将this.$slots
传入一个数组中,变成了嵌套数组,而h
函数是不支持嵌套数组类型的处理的,需要特殊处理传入的slots
- 当前实现了在子组件插槽中插入一个
-
实现思路:可以简单的将传入的
this.$slots
进行h
函数包裹进行展示嵌套数组格式的内容import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js"; export const Foo = { setup() { return {}; }, render() { console.log(this.$slots,'this.$slots=========') const foo = h("p", {}, "foo"); const foo2 = h("h2", {}, "foo2"); return h("div", {}, [foo, foo2,h("div",{},this.$slots)]); }, };
- 渲染结果
- 渲染结果
-
存在的问题:此时
mountElement
只支持数组和TEXT_CHILDREN
类型,而传入单个vnode
到插槽中时是不兼容的,需要将单个vnode
封装成一个数组export function initSlots(instance, children){ // 处理传入的children节点时单节点或者是数组的场景 instance.slots = Array.isArray(children)?children:[children] }
具名插槽与作用域插槽的实现
实现逻辑
具名插槽就是:除了可以有多个,且除了有
default
外,还可以加入其他名字;
作用域插槽:每个slot里面可以传入数据,数据只在当前的slot中有效;父组件进行this
指向控制,从而可以在父组件中访问到子组件中的数据-渲染的插槽元素来自于父组件
- 需求分析
-
具名插槽
-
renderSlot
传入第二个参数,可以获取对应的slots
// 最后还需要使用renderSlot函数 export function renderSlots(slots, name = 'default') { const slot = slots[name] if (slot) { return createVNode('div', {}, slot) } }
-
-
作用域插槽
- 传入插槽的时候,传入一个函数,函数可以拿到子组件传过来的参数
- 只需要在传入插槽的时候进行判断,当slot是一个函数时,执行函数且传入参数
renderSlots
可以传入第三个参数props进行数据接收,用于接收子组件往父组件里传入的参数- 同样做传入内容的判断,是函数时进行传入参数处理
- 传入插槽的时候,传入一个函数,函数可以拿到子组件传过来的参数
-
- 案例分析
// example/componentSlot/App.js
import { h, createTextVNode } from "../../lib/guide-mini-vue.esm.js";
import { Foo } from "./Foo.js";
export const App = {
setup() {
return {};
},
render() {
const foo = h(Foo, {}, h("p", {}, "default slot"));
const foo1 = h(Foo, {}, h("p", {}, {
header:h("p",{},"header"),
footer:h("p",{},"footer")
}));
const foo3 = h(Foo,{},[h("p",{},"123"),h("p",{},"567")])
const foo2 = h("h1", {}, "normal slot");
return h("div", {}, [foo,foo1, foo2,foo3]);
},
};
// example/componentSlot/Foo.js
export const Foo = {
setup() {
return {};
},
render() {
console.log(this.$slots,'this.$slots=========')
const foo = h("p", {}, "foo");
const foo2 = h("h2", {}, "foo2");
return h("div", {}, [foo, foo2,h("div",{},this.$slots)]);
},
};
- 渲染结果 - 主看log
- 实现思路
主要是在子组件中进行slot定向展示时,当遇到具名插槽时就调用单独封装的
renderSlots
函数进行特殊处理,其他类型的slot
是不变的,不调用该方法;
此时子组件调用该方法传入renderSlots
的slot就是一个函数了,参数是插槽的集合、具名插槽的名称、作用域中需要暴漏访问的数据
- 案例分析
export const Foo = {
render(){
const foo = h("P",{},"foo")
console.log(this.$slots,'this.$slots=========')
// 获取到Foo组件中的虚拟节点.vnode children->this.$slots 然后将其渲染出来
// 在渲染节点时 内部的children必须是一个VNode 而下例却是一个数组
// return h("div",{},[foo,renderSlots(this.$slots)])
// 方式一:将传入的数组结构转换成一个个vnode
// h("div",{},[foo,h("div",{},this.$slots)]) - 不建议 粗暴
// 方式二:封装到统一的函数中 由内部导出 renderSlots
// h("div",{},[foo,renderSlots(this.$slots)])
// 需求三 将数组内的组件渲染到指定的位置 - 具名插槽
// 1、获取到要渲染的元素
// 2、获取到要渲染的位置
const age = 12
return h("div",{},[
renderSlots(this.$slots,'header',{
age
}),
foo,
renderSlots(this.$slots,'footer')
])
// return h("div",{},[foo])
},
setup(){
return {}
}
}
import { createVNode, Fragment } from "../vnode";
export function renderSlots(slots,name,props){
// return createVNode("div",{},slots)
const slot = slots[name]
if(slot){
// 在进行传参的时候 slot 就变成了函数 - 作用域插槽
if(typeof slot === 'function'){
// children 是不可以有 Array 的
// return createVNode("div",{},slot(props))
console.log(slot,props,'slot,props=========')
return createVNode(Fragment,{},slot(props))
}
return createVNode(Fragment,{},slot)
}
}
// src/runtime-core/componentSlots.ts
import { ShapeFlags } from "../shared/shapeFlags";
export function initSlots(instance, children){
// 处理传入的children节点时单节点或者是数组的场景
// instance.slots = Array.isArray(children)?children:[children]
// 处理传入的children是一个对象
// const slots = {}
// for (const key in children) {
// if (Object.prototype.hasOwnProperty.call(children, key)) {
// const value = children[key];
// slots[key] = normalizeSlotValue(value)
// }
// };
// instance.slots = slots
const { vnode } = instance
if(vnode.shapeFlag && ShapeFlags.SLOT_COMPONENT){
normalizeObjectSlots(children,instance.slots)
}
}
function normalizeObjectSlots(children,slots:any){
// const slots = {}
for (const key in children) {
if (Object.prototype.hasOwnProperty.call(children, key)) {
const value = children[key];
// slots[key] = normalizeSlotValue(value)
// 作用域插槽
slots[key] = typeof slot !== 'function' ? normalizeSlotValue(value(props)): ((props) => Array.isArray(value(props))?value[props]:[value(props)])
}
};
// instance.slots = slots
}
function normalizeSlotValue(value){
return Array.isArray(value)?value:[value]
}
- sodo