使用观察者模式的思想来高效更新树🌲

314 阅读5分钟

背景

我们团队最近项目中, 有这样的需求

如图所示这是一棵树。

例如下面标红的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"
          }
        ]
      }
    ]
  }
]

总结

我们给每个节点绑定了监听函数, 通过观察者模式,派发来更新🌲, 降低了复杂度, 大大提高了效率。

希望能给大家提供思路,谢谢。

赞一个吧!!!