背景
我们团队最近项目中, 有这样的需求
如图所示这是一棵树。
例如下面标红的A
, A
有N个, 我们想要实现
- 给其中任何一个
A
增加一个子节点, 其他的A
都要增加这个子节点
- 给其中任何一个
A
删除一个子节点, 其他的A
都要删除这个子节点
同时我们并不希望遍历整个树去查找
选择
我们以添加节点为例子来讲
1、遍历整棵树
感觉有点浪费性能
如上需求, 遍历整个树找到其他的A, 给下面增加一个子节点,这是大家首先想到的办法, 要遍历整个, 这个效率比较低, 我们最终pass了, 朝其他方向思考了
2、平铺开, 去操作, 每次去构建树
一听就很麻烦, 太麻烦,效率也低。
3、利用观察者模式(采用)
什么是观察者模式?
观察者模式(Observer), 也叫发布-订阅 (Publish/Subscribe)
定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。UML结构图如下:
观察者先一一注册, 数据变化了, 一一派发更新。
思路
1、给树的每个节点绑定一个函数,并把函数注册监听。
初始化遍历 首先要实现每个节点绑定到函数, 然后把每个节点的函数注册到一个地方。
2、当新增或者删除节点时, 派发更新, 执行节点绑定的注册函数, 更新对应节点的数据。
每当有数据变更的时候,都是页面上的的操作,开发者手动派发更新, 执行对应节点绑定的函数, 更新节点数据
说的可能有点抽象, 来看看具体实现吧
实现
写一个简单事件触发器的类
用来统一收集和派发
class eventEmitter {
// 收集的函数的列表
_func = [];
// 注册监听
on(fn) {
this._func.push(fn);
}
emit() {}
// 派发全部
emitAll() {
this._func.forEach((fn) => {
fn(...arguments);
});
}
// 取消所有的监听
removeAll() {
this._func = []
}
}
造测试数据
图示例
造树, 下面数据的结构, 就是上图
const treeData = [
{
id: 1,
pid: 0,
name: 'A',
children: [
{
id: 4,
pid: 1,
name: 'Z',
children: []
},
{
id: 5,
pid: 1,
name: 'G',
children: []
},
]
},
{
id: 2,
pid: 0,
name: 'B',
children: [
{
id: 6,
pid: 2,
name: 'F',
children: []
},
{
id: 1,
pid: 2,
name: 'A',
children: []
},
]
},
{
id: 3,
pid: 0,
name: 'C',
children: [
{
id: 8,
pid: 3,
name: 'E',
children: []
},
{
id: 1,
pid: 3,
name: 'A',
children: []
},
]
}
]
使用事件触发器
New一个
// new一个事件触发器
const eventEmitter = new EventEmitter();
定义每个节点绑定的函数
比如我要添加子节点 广播进来的id是否等于当前绑定的节点的id,是的话我就插子节点
// 派发执行的函数
function func(id, node) {
// 如果是要更新的节点
if(id == this.id) {
this.children.push(node)
}
}
给每个节点绑定监听函数
初始化的时候遍历整棵树,注册监听
// 遍历树绑定并注册监听函数
const addListener = (data) => {
for(const item of data) {
if (item.children.length) {
addListener(item.children)
}
// 给每个节点绑定监听函数
item._func = func.bind(item)
// 注册
eventEmitter.on(item._func)
}
}
模拟添加节点方法
比如我给A下面添加一个D, 那就广播A的id 1, 和数据给所有订阅者
// 模拟添加节点事件
const onClick_addNode = () => {
eventEmitter.emitAll(1, {
id: 10,
pid: 1,
children: [],
name: 'D'
})
}
执行
// new一个事件触发器
const eventEmitter = new EventEmitter();
// 注册监听
addListener(treeData)
// 模拟给A插数据
onClick_addNode()
// 打印
console.log(treeData)
完整代码
可以复制到浏览器控制台测试一下哦, 看是不下面的结果
class EventEmitter {
// 收集的函数的列表
_func = [];
// 注册监听
on(fn) {
this._func.push(fn);
}
emit() {}
// 派发全部
emitAll() {
this._func.forEach((fn) => {
fn(...arguments);
});
}
// 取消所有的监听
removeAll() {
this._func = []
}
}
// 树的数据
const treeData = [ { id: 1, pid: 0, name: 'A', children: [ { id: 4, pid: 1, name: 'Z', children: []
},
{
id: 5,
pid: 1,
name: 'G',
children: []
},
]
},
{
id: 2,
pid: 0,
name: 'B',
children: [
{
id: 6,
pid: 2,
name: 'F',
children: []
},
{
id: 1,
pid: 2,
name: 'A',
children: []
},
]
},
{
id: 3,
pid: 0,
name: 'C',
children: [
{
id: 8,
pid: 3,
name: 'E',
children: []
},
{
id: 1,
pid: 3,
name: 'A',
children: []
},
]
}
]
// 派发执行的函数
function func(id, node) {
// 如果是要更新的节点
if(id === this.id) {
this.children.push(node)
}
}
// 遍历树绑定并注册监听函数
const addListener = (data) => {
for(const item of data) {
if (item.children.length) {
addListener(item.children)
}
// 给每个节点绑定监听函数
item._func = func.bind(item)
// 注册
eventEmitter.on(item._func)
}
}
// 模拟添加节点事件
const onClick_addNode = () => {
eventEmitter.emitAll(1, {
id: 10,
pid: 1,
children: [],
name: 'D'
})
}
// new一个事件触发器 EventEmitter上面有提到
const eventEmitter = new EventEmitter();
// 注册监听
addListener(treeData) // treeData过长,请到上面自取
// 模拟给A插数据
onClick_addNode()
// 打印
console.log(treeData)
执行结果
图片示例
一图胜千言
我们可以看到给每个A下面都插了 个黄色的D
浏览器控制台执行
控制台执行结果展示, 一张图截不下来, 就放了两张,合起来看
执行后的树
执行结果的树代码示例 下面标黄色部分是增加的
[ { "id": 1, "pid": 0, "name": "A", "children": [ { "id": 4, "pid": 1, "name": "Z", "children": []
},
{
"id": 5,
"pid": 1,
"name": "G",
"children": []
},
{
"id": 10,
"pid": 1,
"children": [],
"name": "D"
}
]
},
{
"id": 2,
"pid": 0,
"name": "B",
"children": [
{
"id": 6,
"pid": 2,
"name": "F",
"children": []
},
{
"id": 1,
"pid": 2,
"name": "A",
"children": [
{
"id": 10,
"pid": 1,
"children": [],
"name": "D"
}
]
}
]
},
{
"id": 3,
"pid": 0,
"name": "C",
"children": [
{
"id": 8,
"pid": 3,
"name": "E",
"children": []
},
{
"id": 1,
"pid": 3,
"name": "A",
"children": [
{
"id": 10,
"pid": 1,
"children": [],
"name": "D"
}
]
}
]
}
]
总结
我们给每个节点绑定了监听函数, 通过观察者模式,派发来更新🌲, 降低了复杂度, 大大提高了效率。
希望能给大家提供思路,谢谢。
赞一个吧!!!