Slot 实现原理
slot
先上代码:(组件定义)
<view>
<!-- 匿名 slot-->
<slot />
<!-- 具名 slot-->
<slot name="footer" />
</view>
编译后的代码:
function defaultRender($data) {
const { $children } = $data
return (
<view>
<$rm.Slot slots={$children} />
<$rm.Slot slots={$children} name="footer" />
</view>
)
}
slot 会被编译成由 runtime 提供的 slot react 组件。并且将当前组件的 children 传过去。
使用组件:
<custom-comp>
<view> defualt </view>
<view slot="footer"> footer </view>
</custom-comp>
编译后代码:
function defaultRender($data) {
return (
<custom-comp>
<view>defualt</view>
<view _slot="footer">footer</view>
</custom-comp>
)
}
上面编译后的代码可以看出,对于适用方来说,编译器并没有做过多的处理。仅仅是将会 slot
属性改成 _slot
,之所以这样,是因为 html
元素本来就有 slot
属性,为了确保不冲突,因此重名了属性名。
slot
不管是组件定义还是适用方,在编译器中并没有太多的转换。实际上,MorJS
中 slot
的实现就是通过 react
组件实现的。runtime
中的 slot 组件代码如下:
export class Slot extends React.PureComponent {
render() {
const { name, slots } = this.props
if (!slots) {
return this.props.children || false
}
const findSlots = []
// NOTE:之所以递归降维到一维数组,是因为有可能slots本身就是一个数组。
flattendeep([slots]).forEach((s) => {
try {
if (s.props._slot === name) {
//具名插槽
findSlots.push(s)
}
} catch (e) {
// 之所以会出现crash的情况,这里为了简化实现方案。比如string、number这些值类型数据,是没有props属性的。
if (!name)
// 只有在默认插槽下面才会显示
findSlots.push(s)
}
})
if (findSlots.length === 0) return this.props.children || false // 直接将slot组件包含的子组件渲染出来
if (findSlots.length === 1) return findSlots[0]
return findSlots
}
}
直接读取子组件的 _slot
属性,跟当前 slot
组件的 name
来匹配。
这里分两种情况:
- 如果当前
slot
是匿名 slot,那么如果_slot
没有设置属性,也就是两者都是undefined
那么匹配成功。 - 如果当前
slot
是具名slot
,那么只有_slot
属性value
跟name
完全一致的情况下才会被匹配。
将匹配到的组件直接由 slot
渲染出来。
另外,也可以分析出,如果 slot
组件没有匹配到任何的子组件,那么直接将 slot
组件包含的子组件渲染出来。
slot-scope
其实 slot-scope
功能才是最复杂的方案。这个功能感觉有点违反了数据单向传输的原则,子元素可以直接访问父组件的数据,打破了 react
中由 setState
维持的单向数据流的壁垒。如果单靠 runtime
的话是无法实现的。
老规矩,直接上示例代码:
组件定义代码:
<!-- 自定义组件中的 -->
<view>
<slot x="{{x}}" name="footer"> default value </slot>
</view>
编译后的代码:
function defaultRender($data) {
const { x, $children } = $data
return (
<view>
<$rm.Slot x={x} slots={$children} name="footer" $scopeKeys={['x']}>
default value
</$rm.Slot>
</view>
)
}
这里相对于上面的普通 slot
实现,仅仅多了一个 $scopeKeys
属性,这个属性的作用仅仅是收集当前 slot
组件中用户定义的需要向外暴露的 key
。
使用组件:
<my-component>
<view slot-scope="props" slot="footer"> component data: {{props.x}} </view>
</my-component>
编译后的代码:
function defaultRender($data) {
const { props } = $data
return (
<my-component>
{function footer(props) {
return <view>{'component data: ' + $rm.getString(props.x)}</view>
}}
</my-component>
)
}
这里可以看出:
slot-scope
直接被编译成一个function
。- 这个
function
的入参就是slot-scope
属性中定义的参数名称。 - 这个
function
的名称就是slot
属性中设置的名称。这里就是“footer”
。 - 这个
function
其实也是一个简单的渲染函数。
这里有意思的是,你可以发现,参数名 props
也被变量提取了。你会发现,有两个地方定义了 props
变量,一个是渲染函数的最开始,另外一个是 slot-scope
编译后的function
的入参。由于变量优先级的存在,function
的入参的变量名大于渲染函数最开始的变量名,因此不会出现变量污染的情况。
除了编译后的代码有不同之外,runtime
中的 slot
组件也需要更新,需要识别 function
,修改的 slot
组件代码如下:
export class Slot extends React.PureComponent {
render() {
const { name, slots } = this.props
if (!slots) {
return this.props.children || false
}
const findSlots = []
// NOTE:之所以递归降维到一维数组,是因为有可能slots本身就是一个数组。
flattendeep([slots]).forEach((s) => {
if (typeof s === 'function') {
// slot是function,那么目前只有一种可能,那就是slot-scope
if (s.name === name || (!name && !s.name)) {
// 具名插槽或者是默认插槽
// 合成数据对象。
const $scopeKeys = this.props.$scopeKeys
const args = {}
if ($scopeKeys && $scopeKeys.length > 0) {
$scopeKeys.forEach((key) => (args[key] = this.props[key]))
}
// slot-scope
// 直接调用函数,并且将数据传入。
findSlots.push(s(args))
}
} else {
try {
if (s.props._slot === name) {
//具名插槽
findSlots.push(s)
}
} catch (e) {
// 之所以会出现crash的情况,这里为了简化实现方案。比如string、number这些值类型数据,是没有props属性的。
if (!name)
// 只有在默认插槽下面才会显示
findSlots.push(s)
}
}
})
if (findSlots.length === 0) return this.props.children || false
if (findSlots.length === 1) return findSlots[0]
return findSlots
}
}
- 判断传入的子组件是否是
function
- 判断是否匹配
slot
中的name
- 通过
$scopeKeys
动态创建一个data
,用来传给function
- 调用
function
获取react
组件