Search code examples
javascriptecmascript-6observer-patternes6-proxy

Alternatives of JavaScript Proxy


I want to use Proxy on a customized class called ObservableList which contains an Array. Since Proxy is available only after ES6, I wonder if there is any alternative implementation.

My requirement is to get updated (rather than get noticed) for observers once ObservableList changes, so that the observers are always consist with observable with some filtering or mapping method.

var activities = new ObservableList(['reading', 'swimming']);
var sAct = activities.filter(function(v) {
  return v[0] === 's';
});
// expect sAct.list to be ['swimming']
var meAct = activities.map(function(v) {
  return 'I am ' + v;
});
// expect meAct.list to be ['I am reading', 'I am swimming']

activities.list.push('smiling');
console.log(sAct.list, meAct.list);
// expect sAct.list to be ['swimming', 'smiling']
// expect meAct.list to be ['I am reading', 'I am swimming', 'I am smiling']

activities.list[1] = 'snoopying';
console.log(sAct.list, meAct.list);
// expect sAct.list to be ['swimming', 'snoopying']
// expect meAct.list to be ['I am reading', 'I am snoopying', 'I am smiling']

My implementation with Proxy is available at https://jsfiddle.net/ovilia/tLmbptr0/3/


Solution

  • As described in questions, I only need ObservableList to contain an Array, rather than to extend it, as Jim did in his complicated answer. And surprisingly enough, I found this could be easily achieved by wrapping the original Array operations.

    One limitation is that index operation is not reactive in my implementation, in that I failed to find a proper way to capture index operations. If you have a better idea, feel welcomed to tell me! XD

    Here's the full implementation.

    export class ObservableList {
    
      list: Array<any>;
    
      private _observer: Array<ObserverList>;
    
      constructor(list?: Array<any>) {
        this.list = list || [];
        this._initList();
        this._initMethods();
    
        this._observer = [];
      }
    
      notify(): void {
        for (let o of this._observer) {
          o.update();
        }
      }
    
      private _initList(): void {
        var that = this;
        var operations = ['push', 'pop', 'shift', 'unshift', 'splice',
          'sort', 'reverse'];
        for (let operation of operations) {
          this.list[operation] = function() {
            var res = Array.prototype[operation].apply(that.list, arguments);
            that.notify();
            return res;
          }
        }
      }
    
      private _initMethods(): void {
        var that = this;
        var methods = ['filter', 'map'];
        for (let method of methods) {
          this[method] = (formatter: Function): ObserverList => {
            var observer = new ObserverList(that, formatter, method);
            this._observer.push(observer);
            return observer;
          }
        }
      }
    
    }
    
    export class ObserverList {
    
      public list: Array<any>;
    
      constructor(public observable: ObservableList, 
                  public formatter: Function, 
                  public method: string) {
        this.list = [];
        this.update();
      }
    
      update(): void {
        var list = [];
        var master = this.observable.list;
        for (var i = 0, len = master.length; i < len; ++i) {
          if (this.method === 'filter') {
            if (this.formatter(master[i])) {
              list.push(master[i]);
            }
          } else if (this.method === 'map') {
            list.push(this.formatter(master[i]));
          } else {
            console.error('Illegal method ' + this.method + '.');
          }
        }
        this.list = list;
      }
    
    }