JavaScript设计模式之组合模式
概念
在《JavaScript设计模式与开发实践》中是这样描述组合模式的:组合模式就是用小的子对象来构建成更大的对象,而这些小的子对象本身也许是更小的“孙对象”构成。
命令模式的例子
回顾上上一节命令模式,我们假设定义了若干个对象,需要去调用对应的execute
函数来执行对应的命令,举一个书中的例子:
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
}
var openPCCommand = {
execute: function(){
console.log( '开电脑' );
}
}
var openQQCommand = {
execute: function(){
console.log( '登陆QQ' );
}
}
上面我们定义了三个命令,分别为关门、开电脑、登陆qq,那么假设我们每天都需要做这三个操作呢?我们就需要编写一个宏命令函数:
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[i++];){
command.execute();
}
}
}
};
接下来,我们执行如下操作,将三个子命令添加到MacroCommand
中:
var macroCommand = MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPCCommand)
macroCommand.add(openQQCommand)
macroCommand.execute()
观察代码,我们会发现,宏命令和子命令组成了一个树形结构。macroCommand对象称为组合对象,closeDoorCommand、openPCCommand、openQQCommand
都是叶对象。
在execute
执行时,函数将会遍历commandsList
中的所有子命令,并调用每个子命令的execute
方法。而这种模式在开发中会带来相当大的便利性,因为,当我们新增一个命令时,我们并不需要关心他是宏命令还是子命令,只要关心他是否拥有可执行的execute
函数就可以了。
在上图中,我们发现,如果节点是叶对象,那么将自行处理execute
,如果子节点还是组合对象,那么将继续向下传递,直到树的尽头。
所以,可以总结出:组合模式的核心是通过递归的方式访问整个树形结构。
源码中的组合模式
antd vue的Tree组件
前面刚说到:组合模式的核心就是递归处理树形结构。那么就很容易想到树形结构的最典型组件:
Tree
组件。那么我们来看下antd组件中是如何实现的。
根组件
为了更直观的看到组件的递归方式,我们传入如下数据结构:
const treeData = [
{
title: '0-0',
key: '0-0',
children: [
{
title: '0-0-0',
key: '0-0-0',
children: [
{ title: '0-0-0-0', key: '0-0-0-0' },
{ title: '0-0-0-1', key: '0-0-0-1' },
{ title: '0-0-0-2', key: '0-0-0-2' },
],
},
{
title: '0-0-1',
key: '0-0-1',
children: [
{ title: '0-0-1-0', key: '0-0-1-0' },
{ title: '0-0-1-1', key: '0-0-1-1' },
{ title: '0-0-1-2', key: '0-0-1-2' },
],
},
{
title: '0-0-2',
key: '0-0-2',
},
],
},
{
title: '0-1',
key: '0-1',
children: [
{ title: '0-1-0-0', key: '0-1-0-0' },
{ title: '0-1-0-1', key: '0-1-0-1' },
{ title: '0-1-0-2', key: '0-1-0-2' },
],
},
{
title: '0-2',
key: '0-2',
},
];
并设置选中key
为['0-0-0']
,此时,可以看到Tree组件的展示效果:
对应在antd源码中,我们的根组件代码在vc-tree/Tree.jsx
,代码如下:
render() {
const { _treeNode: treeNode } = this.$data;
const { prefixCls, focusable, showLine, tabIndex = 0 } = this.$props;
return (
<ul
class={classNames(prefixCls, {
[`${prefixCls}-show-line`]: showLine,
})}
role="tree"
unselectable="on"
tabIndex={focusable ? tabIndex : null}
>
{mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
</ul>
);
},
在源码中可以看到根组件有一个标识role=tree
, 通过mapChildren
遍历渲染其子节点。
遍历子节点
先来看下mapChildren
函数:
export function mapChildren(children = [], func) {
const list = children.map(func);
if (list.length === 1) {
return list[0];
}
return list;
}
非常简单,遍历treeNode
,然后执行renderTreeNode
渲染子节点。那么treeNode
是什么呢?
查看组件的data数据,可以发现是在getDerivedState
中对treeNode
进行赋值的,为了简化逻辑,直接贴出treeNode
赋值的函数:
// vc-tree/src/Tree.jsx
getDerivedState(props, prevState) {
...
if (needSync('treeData')) {
treeNode = convertDataToTree(this.$createElement, props.treeData);
}
...
}
// vc-tree/src/util.js
export function convertDataToTree(h, treeData, processor) {
if (!treeData) return [];
const { processProps = internalProcessProps } = processor || {};
const list = Array.isArray(treeData) ? treeData : [treeData];
return list.map(({ children, ...props }) => {
const childrenNodes = convertDataToTree(h, children, processor);
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
});
}
通过convertDataToTree
函数,我们可以看到,组件中会将我们传入的treeData
进行递归处理,返回树形结构的VNode
,如图:
TreeNode渲染
查看TreeNode组件源码:
render(h) {
const {
dragOver,
dragOverGapTop,
dragOverGapBottom,
isLeaf,
expanded,
selected,
checked,
halfChecked,
loading,
} = this.$props;
const {
vcTree: { prefixCls, filterTreeNode, draggable },
} = this;
const disabled = this.isDisabled();
return (
<li
class={{
[`${prefixCls}-treenode-disabled`]: disabled,
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
[`${prefixCls}-treenode-checkbox-checked`]: checked,
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
[`${prefixCls}-treenode-selected`]: selected,
[`${prefixCls}-treenode-loading`]: loading,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(this),
}}
role="treeitem"
onDragenter={draggable ? this.onDragEnter : noop}
onDragover={draggable ? this.onDragOver : noop}
onDragleave={draggable ? this.onDragLeave : noop}
onDrop={draggable ? this.onDrop : noop}
onDragend={draggable ? this.onDragEnd : noop}
>
{this.renderSwitcher()}
{this.renderCheckbox()}
{this.renderSelector(h)}
{this.renderChildren()}
</li>
);
},
可以看到几个关键点:
TreeNode
组件由一个li
标签包裹,并且role=treeitem
<li role="treeitem">
标签中有renderSwitcher、renderCheckbox、renderSelector、renderChildren
。其中renderSelector
显示树形节点的图标和标题,renderChildren
显示组件下的子组件,并且由<ul role="group">
包裹- 在
<ul role="group">
遍历渲染多个TreeNode
组件
对应下图可更清晰的看到其结构:
其中,蓝色表示TreeNode
节点,橙色表示节点中的图标、title、checkbox等基本信息,紫色表示子节点的根元素也就是ul
,内部包含着对应的子节点。
由于遍历渲染
TreeNode
组件,操作子节点时参数和方法保持统一,从而提高了组件的可扩展性,这就是组合模式的一个典型应用。
总结
本文介绍了组合模式的概念和应用,以及在 Antd Vue 的 Tree 组件中的应用。组合模式的核心是通过递归的方式访问整个树形结构。在 Antd Vue 的 Tree 组件中,通过遍历渲染 TreeNode 组件并保持参数和方法的统一,操作子节点时提高了组件的可扩展性。
总的来说,组合模式是处理复杂树形结构的有用设计模式,其应用不仅局限于用户界面组件。
感谢阅读🙏
转载自:https://juejin.cn/post/7210746685802217531