如何在JavaScript中实现观察者模式?

18

嗨,我正在尝试在JavaScript中实现观察者模式:

我的 index.js:

$(document).ready(function () {
  var ironMan = new Movie();
  ironMan.setTitle('IronMan');
  ironMan.setRating('R');
  ironMan.setId(1);
  //  ironMan.setCast(['Robert Downey Jr.', 'Jeff Bridges', 'Gwyneth Paltrow']);

  var terminator = new Movie();
  terminator.setTitle('Terminator');
  terminator.setRating('P');
  terminator.setId(2);

  console.log(ironMan.toString());
  console.log(terminator.toString());

  ironMan.play();
  ironMan.stop();
  ironMan.download();
  ironMan.share('V. Rivas');

  console.log(ironMan.getCast()[0]);
});

我的电影:

var title;
var rating;
var id;
var observers;


function Movie() {
  observers = new ObserverList();
}

//function Movie (title, rating, id){
//  this. title = title;
//  this.rating =  rating;
//  this.id =id;
//  observers = new ObserverList();
//}

Movie.prototype.setTitle = function (newTitle) {
  this.title = newTitle;
}

Movie.prototype.getTilte = function () {
  return this.title;
}

Movie.prototype.setRating = function (newRating) {
  this.rating = newRating;
}

Movie.prototype.getRating = function () {
  return this.rating;
}

Movie.prototype.setId = function (newId) {
  this.id = newId;
}

Movie.prototype.getId = function () {
  return this.id;
}

Movie.prototype.play = function () {
  for (i = 0; i < observers.Count; i++) {
    console.log("palying...");
  }
}

Movie.prototype.stop = function () {
  for (i = 0; i < observers.Count; i++) {
    console.log("stoped");
  }
}

Movie.prototype.AddObserver = function (observer) {
  observers.Add(observer);
};

最后的观察者:

function ObserverList() {
  this.observerList = [];
}

ObserverList.prototype.Add = function (obj) {
  return this.observerList.push(obj);
};

ObserverList.prototype.Empty = function () {
  this.observerList = [];
};

ObserverList.prototype.Count = function () {
  return this.observerList.length;
};

ObserverList.prototype.Get = function (index) {
  if (index > -1 && index < this.observerList.length) {
    return this.observerList[index];
  }
};

ObserverList.prototype.Insert = function (obj, index) {
  var pointer = -1;

  if (index === 0) {
    this.observerList.unshift(obj);
    pointer = index;
  } else if (index === this.observerList.length) {
    this.observerList.push(obj);
    pointer = index;
  }

  return pointer;
};

非常感谢您能提供的任何帮助。

10个回答

30

JavaScript是基于事件驱动的:这意味着它知道时间并且期望事物随时间变化。最初的观察者模式是为诸如C ++之类不知道时间的语言创建的。 您可以通过使用游戏循环来检查状态更改来利用JavaScript的优势。

创建两个DOM元素,一个输入和一个输出

<input type="text" value="Enter some text...">
<p id="output">

建立一个requestAnimationFrame循环并开始观察。

//Get a reference to the input and output
var input = document.querySelector("input");
var output = document.querySelector("#output");

//Set up a requestAnimationFrame loop
function update () {
  requestAnimationFrame(update);
  
  //Change the output to match the input
  output.innerHTML = input.value;
}
update(); 

这就是游戏引擎用于立即模式渲染的功能。这也是React框架用于检查DOM状态更改的方式。

(如果需要,这里有一个简单的requestAnimationPolyfill)

//Polyfill for requestAnimationFrame
window.requestAnimationFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          window.oRequestAnimationFrame      ||
          window.msRequestAnimationFrame     ||
          function(/* function */ callback, /* DOMElement */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

14
在JavaScript中,没有必要像Java一样实现纯观察者模式,因为JavaScript有函数式编程。所以,只需要使用类似于 http://api.jquery.com/category/callbacks-object/ 的东西来代替你的ObserverList。
如果你仍然想使用你的对象,那么一切都取决于你想传递给ObserverList.Add的内容。如果是一些对象,那么你需要写:
for( i = 0; i < observers.Count; i++) { 
  observers[i].Notify("some data"); 
}

如果它是一个函数,那么你需要编写

for( i = 0; i < observers.Count; i++) { 
  observers[i]("Some data"); 
}

你也可以使用Function.apply()或Function.call()来为你的函数提供this


我会检查一下,但如果我想让它按照原样工作,我该怎么做? - Ignacio Garat
嗨,谢谢 Dmitry。我正在从 Movie.AddObserver 调用 ObserverList.Add 表单,在哪里应该调用 Movie.AddObserver,需要传递哪些参数?非常感谢 Dmitry! - Ignacio Garat
有些情况下广播模式会非常有用。例如,假设你有一个包含一堆对象的接口,这些对象根据一个元素的更改而自我修改(比如某种复杂计算器)。这时候如果能够广播一个像 factorChanged 这样的事件,每个表单元素都可以监听该事件并相应地进行自我调整,那么就会非常方便了。 - Jordan Reiter
7
观察者模式不能被回调所替代。观察者模式的思想是,你可以看到对象何时发生变化,而不仅仅是在需要时触发事件或调用回调函数。 - B T
1
@mikerodent,实现观察者模式的方法有很多种,但是观察者模式本身并不同于发布/订阅模式。观察者模式是发布/订阅模式的一个子集 - 它更加具体。 - B T
显示剩余5条评论

4
这是一个在JavaScript中实现观察者模式的例子,提供了与Backbone Models非常相似的API。该实现避免使用"this"和"new",正如Douglas Crockford建议的那样。
// The constructor function.
function Model(){

  // An object containing callback functions.
  //  * Keys are property names
  //  * Values are arrays of callback functions
  var callbacks = {},

      // An object containing property values.
      //  * Keys are property names
      //  * Values are values set on the model
      values = {};

  // Return the public Model API,
  // using the revealing module pattern.
  return {

    // Gets a value from the model.
    get: function(key){
      return values[key];
    },

    // Sets a value on the model and
    // invokes callbacks added for the property,
    // passing the new value into the callback.
    set: function(key, value){
      values[key] = value;
      if(callbacks[key]){
        callbacks[key].forEach(function (callback) {
          callback(value);
        });
      }
    },

    // Adds a callback that will listen for changes
    // to the specified property.
    on: function(key, callbackToAdd){
      if(!callbacks[key]){
        callbacks[key] = [];
      }
      callbacks[key].push(callbackToAdd);
    },

    // Removes a callback that listening for changes
    // to the specified property.
    off: function(key, callbackToRemove){
      if(callbacks[key]){
        callbacks[key] = callbacks[key].filter(function (callback) {
          return callback !== callbackToRemove;
        });
      }
    }
  };
}

这里是使用 Model 的示例代码:

// Create a new model.
var model = Model();

// Create callbacks for X and Y properties.
function listenX(x){
  // The new value is passed to the callback.
  console.log('x changed to ' + x);
}

function listenY(y){
  // The new value can be extracted from the model.
  console.log('y changed to ' + model.get('y'));
}

// Add callbacks as observers to the model.
model.on('x', listenX);
model.on('y', listenY);

// Set values of X and Y.
model.set('x', 30); // prints "x changed to 30"
model.set('y', 40); // prints "y changed to 40"

// Remove one listener.
model.off('x', listenX);
model.set('x', 360); // prints nothing
model.set('y', 50); // prints "y changed to 40"

这是对象字面量模式。对于暴露模块模式,函数getset都将在闭包中定义,并且你会在形式末尾返回一个对象,形如 return {get: get, set: set} - I-Lin Kuo
你的解决方案更像是发布-订阅模式。https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern - Alex P.

3
class EventObserver {
  constructor () {
    this.observers = []
  }

  subscribe (fn) {
    this.observers.push(fn)
  }

  unsubscribe (fn) {
    this.observers = this.observers.filter(subscriber => subscriber !== fn)
  }

  broadcast (data) {
    this.observers.forEach(subscriber => subscriber(data))
  }
}

或者你可以在NodeJs中使用EventEmitter https://nodejs.org/api/events.html#events_class_eventemitter


不知道 JavaScript 的类型非常困惑,但这个实现非常干净和有帮助。谢谢! - Lucas Sousa

2

class Observable {
    constructor() {
       this.observer = []; 
    }
    subscribe(item) {
        this.observer.push(item);
    }
    unsubscribe(item) {
        if(!this.observer) return 'empty';
        else {
            this.observer.filter(subscribe => subscribe !== item);
        }
    }
    notify(data) {
        this.observer.forEach(item => item(data));
    }
}

var p1 = document.querySelector('.p1');
var p2 = document.querySelector('.p2');
var p3 = document.querySelector('.p3');
var input = document.querySelector('.input');

const update1 = text => p1.textContent = text;
const update2 = text => p2.textContent = text;
const update3 = text => p3.textContent = text;

var observarble = new Observable();
observarble.subscribe(update1);
observarble.subscribe(update2);
observarble.subscribe(update3);

input.addEventListener('keyup', event => observarble.notify(event.target.value));
<input type="input" class="input" />
<div class="p1"></div>
<div class="p2"></div>
<div class="p3"></div>

Observer是所有JavaScript应用程序中广泛使用的流行模式之一。

实例(subject)维护一个对象(观察者)集合,并在状态发生变化时通知它们所有人。

让我们编写一些逻辑来解释。

class Observable {
    constructor() {
       this.observer = []; 
    }
    subscribe(item) {
        this.observer.push(item);
    }
    unsubscribe(item) {
        if(!this.observer) return 'empty';
        else {
            this.observer.filter(subscribe => subscribe !== item);
        }
    }
    notify(data) {
        this.observer.forEach(item => item(data));
    }
}

现在你的问题是下一步是什么?接下来应该在哪里使用这个模式呢?
当某些事件发生时,需要同时更新多个元素时,可以考虑使用这种模式。
请在代码中添加一些HTML。
<input type="input" class="input" />
<div class="p1"></div>
<div class="p2"></div>
<div class="p3"></div>

使用Javascript获取这些节点
var p1 = document.querySelector('.p1');
var p2 = document.querySelector('.p2');
var p3 = document.querySelector('.p3');
var input = document.querySelector('.input');

使用观察者设置值时,需要添加它们的文本内容。
const update1 = text => p1.textContent = text;
const update2 = text => p2.textContent = text;
const update3 = text => p3.textContent = text;


var observarble = new Observable();
observarble.subscribe(update1);
observarble.subscribe(update2);
observarble.subscribe(update3);

最后一件事是在输入框上添加一个事件监听器,监听keyup/change事件。

input.addEventListener('keyup', ev => observarble.notify(ev.target.value));

就是这样 :) !!

工作演示链接 https://codepen.io/nishant5857/pen/MWKdByY


const update1 = text => p1.textContent = text; - 这是关键点,它在执行回调函数时包括this上下文,并跟踪unsubscribe任务。 - Nguyen Tran

2
以下是我从《学习JavaScript设计模式》一书中稍作改编的实现。
function pubsub(obj) {

var events = {},
    subUid = -1;

obj.publish = function (event, args) {

    if (!events[event]) {
        return false;
    }

    var subscribers = events[event],
    len = subscribers ? subscribers.length : 0;

    while (len--) {
        subscribers[len].func(event, args);
    }
};

obj.subscribe = function (event, func) {

    if (!events[event]) {
        events[event] = [];
    }

    var token = (++subUid).toString();
    events[event].push({
        token: token,
        func: func
    });

    return token;

};

obj.unsubscribe = function (token) {

    for (var event in events) {
        if (events.hasOwnProperty(event)) {
            for (var i = 0, j = events[event].length ; i < j ; i++) {
                if (events[event][i].token === token) {
                    events[event].splice(i, 1);
                }
            }
        }
    }

    return this;

};

}

var obj = {}; // Any javascript object
pubsub(obj); // Make an observable from the given object

var subscription = obj.subscribe('load', handler);

// event handler callback
function handler(event, data) {
    console.log(event, data);
}

obj.publish('load', 'Data loaded successfully'); // logs 'load Data loaded successfully'
obj.unsubscribe(subscription);
obj.publish('load', 'Data loaded successfully'); // nothing happens

干杯!


0

对我来说,这是在JS中实现观察者模式的最佳方式。

function Click() {
    this.handlers = [];  // observers
}

Click.prototype = {

    subscribe: function(fn) {
        this.handlers.push(fn);
    },

    unsubscribe: function(fn) {
        this.handlers = this.handlers.filter(
            function(item) {
                if (item !== fn) {
                    return item;
                }
            }
        );
    },

    fire: function(o, thisObj) {
        var scope = thisObj || window;
        this.handlers.forEach(function(item) {
            item.call(scope, o);
        });
    }
}

// log helper

var log = (function() {
    var log = "";

    return {
        add: function(msg) { log += msg + "\n"; },
        show: function() { alert(log); log = ""; }
    }
})();

function run() {

    var clickHandler = function(item) { 
        log.add("fired: " + item); 
    };

    var click = new Click();

    click.subscribe(clickHandler);
    click.fire('event #1');
    click.unsubscribe(clickHandler);
    click.fire('event #2');
    click.subscribe(clickHandler);
    click.fire('event #3');

    log.show();
}

如果附加了具有相同功能的多个侦听器会发生什么?假设methodA和methodB都订阅了相同的函数。 - Rafael Herscovici

0

不必自己实现观察者模式,你可以使用 RxJS 提供的 Observable API。

一个非常简单的版本,不依赖于完整的库,但是可以让你在将来迁移到整个 RxJS 栈时使用,可以按照以下方式编写(它不尝试完成和错误回调)

/**
 * Basic Observable, made compatible with RX-JS API.
 */
type Observer<T> = {
  next: (value: T) => void;
};

interface Subscription {
  unsubscribe(): void;
}

export class Observable<T> {
  private observers: Observer<T>[] = [];
  subscribe(next: (value: T) => void): Subscription {
    const observer = { next }
    this.observers.push(observer);
    return {
      unsubscribe: () => {
        this.observers = this.observers.filter((h) => h !== observer);
      },
    };
  }
  next(data: T) {
    this.observers.forEach((h) => h.next(data));
  }
}

并进行了以下测试:

import { Observable } from './Observable';
import noop from 'noop-fn';
describe('Observable', () => {
  it('can be created', () => {
    new Observable();
  });
  it('can be subscribed to', () => {
    const o = new Observable();
    const subscription = o.subscribe(noop);
    subscription.unsubscribe();
  });
  it('can receive notifications', () => {
    const o = new Observable();
    const handler = jest.fn();
    const subscription = o.subscribe(handler);
    o.next('Hello');
    subscription.unsubscribe();
    expect(handler.mock.calls.length).toBe(1);
    expect(handler.mock.calls[0][0]).toBe('Hello');
  });
});

0

这篇文章虽然有些旧,但我想提供一个答案来回应原始问题:“如何在现有代码的基础上实现观察者模式”。

观察者模式可以简化为一种通信设计,其中目标(被观察的对象)具有指向观察者的指针,并假定观察者具有公共API。例如,目标假定观察者具有名为update的方法,或者观察者是一个Function。这是目标通过在观察者对象上调用方法(或Function,如果观察者是函数)来通知观察者变化的方式。

每当属性被改变时,目标必须update所有已注册要接收通知的观察者。

在这里,我假设您想知道如何实现键值观察器。在这种情况下,代码将必须遍历其观察者列表,并在属性更改时调用每个观察者的update方法(或仅在观察者是Function的情况下执行观察者)。

var observers = null;

function Movie() {
 observers = new ObserverList();
}

Movie.prototype.changed = function(key, old, value){
  // Assumption here is that observers can observe individual properties.
  if(!this.observers[key]) return
  
  this.observers[key].forEach( o => {
    // Assumption is that observers have an update method. This is the only
    // thing the target knows about an observer.
      o.update(key, old, value, this)
  })

}

// Now every setter on the target has to notify the observers by calling `changed`
Movie.prototype.setTitle = function (newTitle) {
  var old = this.title;
  this.title = newTitle;
  this.changed("title", old, this.title, this)
}

你需要添加那个changed方法,然后更新所有的setter方法,按照上面的调用changed。

我还注意到原始代码中没有任何地方observes电影。你需要添加代码来实际observes它,实现类似上面例子中给出的update的东西。

我不确定对于playstop的意图是什么。


0
观察者模式是关于更新对象并自动发送事件以提供有关更新内容的信息。例如:
function ObserverList(){
  this.observerList = []
  this.listeners = []
}

ObserverList.prototype.add = function( obj ){
  this.observerList.push(obj)
  this.listeners.forEach(function(callback){
    callback({type:'add', obj:obj})
  })
}

ObserverList.prototype.onChange = function(callback){
  this.listeners.push(callback)
}

这是一个JavaScript中观察者模式的模块,您可以查看源代码以获取更多信息:https://github.com/Tixit/observe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接