JavaScript中的观察者模式--响应式行为的关键

335 阅读7分钟

原文链接:blog.bitsrc.io/the-observe…

仔细看看我最喜欢的设计模式之一,以及为什么它在今天的响应式世界中如此相关。

许多新的开发者倾向于在React等框架面前添加一层魔法的面纱,因为他们看到数据流的走向,以及它与他们在成为新的开发者时学到的一切有多大的不同。

而这是真的,如果你不知道发生了什么,它就像魔法一样,但就像Arthur C. Clarke曾经说过。

任何足够先进的技术都与魔法无异

因此,让我们仔细看看你在实践中可能发现的任何反应式行为背后的基本原理,以及为什么它(a)不是魔术,(b)理解起来如此有用。

提醒你,我不会假装把React过度简化为一个单一的模式就能让你理解整个框架的工作原理。我只是在谈论一个可以跨越多种用例的单一原则,其中之一就是前端框架。

观察者模式解析

让我们从头开始:你需要了解的第一件事,是模式本身。

相信我,一旦你这样做了,你就会发现根本没有什么魔力(抱歉戳破你的幻想!)。

这就是所谓的 "行为模式",即它涉及到对象的行为,以及当事情发生时它们如何相互作用。

也就是说,该模式显示了当一组对象(观察者)对另一个单一对象(被观察者)的状态的任何类型的变化感兴趣时,如何构建观察者-被观察者类型的关系。

但这里的关键是,"观察者 "并没有主动观察被观察的对象,相反,他们订阅了通知,一旦有事情发生,他们会让被观察的元素通知他们。

这个小细节很关键,因为如果你主动观察,就意味着你必须花费计算周期进行某种检查。虽然这对单个对象来说可能不是什么大事,但如果你扩大到几百个(或可能是几千个)观察者,所需的时间就变得很重要了。所以是的,这是一个问题。

如果相反,这些观察者可以自由地继续工作,或者甚至只是在等待时闲置而不花费处理周期,那么这个模式就会成为一个性能梦想。

如果你还在挣扎,可以想想这就像你自己去拿你的日报与订阅报纸并把它送到你的家门口。你会花更多的精力和时间每天去那里,而不是等待他们把报纸送到你面前。

上面的图并不是试图用UML来表示这个模式(我在看你们这些纯粹主义者!),而是一个简单的例子,说明3个观察者如何与被观察对象进行交互。就是这么简单。你可以看到每个观察者如何从外部调用addSubscriber方法,以及notifySubscribers方法如何用一个特定的event参数来调用他们的update方法。即使是这样,也不过是刚刚发生变化的细节。你可以改变这一点,让观察者直接访问被观察对象的状态,但我认为这种方式更容易,因为你特别向他们展示了什么变化(即什么触发了通知)。

现在你可以开始看到,这种模式背后并没有真正的魔法,它只是如此优雅,以至于从观察者的角度来看(在某些情况下是开发者所站的位置),它看起来像魔法。

用JavaScript实现观察者模式

现在让我们开始编写一些代码! 在这个例子中,我假设我们有某种循环,我们在迭代一个变量的值,我们想在值满足特定条件时做出反应。 例如,假设有一个从1到1000的循环,并希望确保我们对每个奇数做出反应。按照我目前所说的,你会得到这样的结果:

现在,我们在这里处理的是JavaScript,所以我们必须记住,这里没有“私有”方法或属性的概念(至少现在还没有),甚至没有抽象类或方法。所以我们在实现的时候只能见机行事了。

然而,我们可以实现一些东西,让我们像这样工作:

const Looper = require('./looper');
const OddNotifier = require('./oddNotifier');

const l = new Looper(1, 100000);
l.addObserver(new OddNotifier());
l.run();

非常直接的实现,但你会明白的。Lopper类将获取循环的起点和终点,并通过run方法运行它。而在这之前,我们可以通过addObserver方法插入任意多的观察者。了解观察者何时以及如何反应所需的逻辑被封装在每个观察者(在本例中是OddNotifier)中。

运行这段代码会产生如下的输出:

如你所见,我们实现了一个非常恼人的观察者。

现在让我们来看看这个逻辑:

const Observer = require('./observer');

class OddNotifier extends Observer {
  constructor() {
    super();
  }

  eventIsRelevant(evnt) {
    return evnt.evntName == 'new-index' && evnt.value % 2 != 0;
  }

  reactToEvent(evnt) {
    console.log('----------------------');
    console.log('Odd number found!');
    console.log(evnt.value);
    console.log('----------------------');
  }
}

正如你所看到的,我们已经重新实现了eventIsRelevantreactToEvent方法,这些方法也是在Observer父类上定义的(根照我们的UML图)。在这里,我们很清楚地检查了事件的名称和值。如果它符合我们的标准,则返回true。注意这里我们没有实现update方法。它不需要被覆盖,因为作为父类的一部分的基本实现就足够了。如果合适的话,父类将负责调用reactToEvent。现在让我们快速看一下:

class Observer {
  update(event) {
    if (this.eventIsRelevant(event)) {
      this.reactToEvent(event);
    }
  }

  eventIsRelevant() {
    throw new Error('This needs to be implemented');
  }

  reactToEvent() {
    throw new Error('This needs to be implemented');
  }
}

module.exports = Observer;

这里没有太多花哨的东西,我定义了一个非常简单的update方法,它接收一个事件,如果它是相关的,它将对它做出反应。不管这意味着什么,都要留给每个具体的观察者来定义(正如我们已经看到的)。

至于我们关注的对象,我们的`Looper'类,它将负责迭代这些值--正如它非常复杂的逻辑所决定的那样--并且每次有新的事件需要被触发时,它将通知观察者。看一下吧:

const Subject = require('./subject');

module.exports = class Looper extends Subject {
  constructor(first, last) {
    super();
    this.start = first;
    this.state = first;
    this.end = last;
  }

  run() {
    for (this.state = this.start; this.state < this.end; this.state++) {
      this.notifyObservers({
        evntName: 'new-index',
        value: this.state,
      });
    }
  }
};

代码不关心如何添加新的观察者,也不关心通知他们意味着什么。它只需要知道有一个名为notifyObservers的方法,并且需要在每个相关事件上调用这个方法。在这个例子中,它被翻译成“在我的循环的每个新值上”。但其实也可能是其他任何东西。事实上,如果逻辑更复杂,我们也可以有多个事件被触发,并且根据每个具体观察者内部的逻辑,他们将能够决定事件是否与他们相关。

最后,Subject类非常简单,因为它只需要担心收集和通知观察者:

class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(obs) {
    this.observers.push(obs);
  }

  notifyObservers(event) {
    this.observers.forEach((o) => o.update(event));
  }
}

module.exports = Subject;

说实话,没有比这更简单的了。

现在,让我们再进一步,试着展示一个更贴近实际的例子,尤其是当你是React开发者的时候。

想象一下如果你能做这样的事情:

let [looper, increaseLooper] = useState(1);

console.log('Initial state: ', looper.state);

console.log('Increasing the value by 1');
increaseLooper();
console.log('Increasing the value by 1');
increaseLooper();
console.log('Increasing the value by 1');
increaseLooper();

我已经创建了一个“钩子”,它将变量looper的初始状态设置为1,并返回一个每次增加1的函数。除了能够将我们的内部循环器的状态增加1之外,你认为我是否拥有这个 "魔法 "所需的一切?

因为它确实有效:

下面是我们实现的钩子:

function useState(start) {
  let l = new Looper(start, start * 10000);

  l.addObserver(new OddNotifier());

  let fn = () => {
    l.increase();
  };

  return [l, fn];
}

既然我们知道了规律就不奇怪了,对吧?新的increase 方法就是这样:

//....
    increase() {
        this.state++;
        this.notifyObservers({
            evntName: "new-index",
            value: this.state
        })
    }
//...

我正在更新内部状态并通知所有观察员。这就是我们所需要的。

既然观察者模式的“面纱”被揭开了,"钩子 "的神秘行为只不过是预先设定好的观察者集合。你对此有何看法?

你以前使用过这种模式吗?你用Node.js的eventEmitter试过吗?这使得它更容易实现,因为那个模块为我们做了所有的重活!你觉得呢?

在评论中分享你最喜欢的设计模式或你想更好地了解的设计模式,我将在下一次尝试介绍它!