Как использовать прокси-сервер javascript для вложенных объектов

У меня есть этот код в js bin:

var validator = {
  set (target, key, value) {
    console.log(target);
    console.log(key);
    console.log(value);
    if(isObject(target[key])){

    }
    return true
  }
}


var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

Если я делаю proxy.inner.salary = 555;, он не работает.

Однако, если я делаю proxy.firstName = "Anne", то он отлично работает.

Я не понимаю, почему это не работает. Рекурсивно.

http://jsbin.com/dinerotiwe/edit?html,js,console

Ответ 1

Вы можете добавить ловушку get и вернуть новый прокси с validator в качестве обработчика:

var validator = {
  get(target, key) {
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], validator)
    } else {
      return target[key];
    }
  },
  set (target, key, value) {
    console.log(target);
    console.log(key);
    console.log(value);
    return true
  }
}


var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

Ответ 2

Я опубликовал библиотеку на GitHub, которая также делает это. Он также сообщит функции обратного вызова, какие изменения были внесены вместе с их полным путем.

Михал хороший ответ, но он создает новый Proxy каждый раз, когда к вложенному объекту обращаются. В зависимости от вашего использования это может привести к очень большой нагрузке на память.

Ответ 3

Небольшая модификация на примере Михаэля Перлаковского, благодаря чему этот подход заключается в том, что вложенный прокси-сервер создается только один раз, а не каждый раз, когда происходит доступ к значению.

Если свойство прокси, к которому осуществляется доступ, является объектом или массивом, значение свойства заменяется другим прокси. Свойство isProxy в геттере используется для определения того, является ли объект, к которому осуществляется доступ в данный момент, прокси или нет. Возможно, вы захотите изменить имя isProxy, чтобы избежать именования коллизий со свойствами сохраненных объектов.

Примечание: вложенный прокси-сервер определяется в получателе, а не в установщике, поэтому он создается только в том случае, если данные действительно где-то используются. Это может или не может соответствовать вашему варианту использования.

const handler = {
  get(target, key) {
    if (key == 'isProxy')
      return true;

    const prop = target[key];

    // return if property not found
    if (typeof prop == 'undefined')
      return;

    // set value as proxy if object
    if (!prop.isProxy && typeof prop === 'object')
      target[key] = new Proxy(prop, handler);

    return target[key];
  },
  set(target, key, value) {
    console.log('Setting', target, '.${key} to equal', value);

    // todo : call callback

    target[key] = value;
    return true;
  }
};

const test = {
  string: "data",
  number: 231321,
  object: {
    string: "data",
    number: 32434
  },
  array: [
    1, 2, 3, 4, 5
  ],
};

const proxy = new Proxy(test, handler);

console.log(proxy);
console.log(proxy.string); // "data"

proxy.string = "Hello";

console.log(proxy.string); // "Hello"

console.log(proxy.object); // { "string": "data", "number": 32434 }

proxy.object.string = "World";

console.log(proxy.object.string); // "World"

Ответ 4

Я также создал функцию типа библиотеки для наблюдения за обновлениями в глубоко вложенных объектах прокси (я создал ее для использования в качестве односторонней связанной модели данных). По сравнению с библиотекой Эллиота это немного легче понять при < 100 строк. Более того, я думаю, что Эллиот беспокоится о новых объектах Proxy, которые были сделаны, является преждевременной оптимизацией, поэтому я сохранил эту функцию, чтобы упростить рассуждение о функции кода.

observable-model.js

let ObservableModel = (function () {
    /*
    * observableValidation: This is a validation handler for the observable model construct.
    * It allows objects to be created with deeply nested object hierarchies, each of which
    * is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
    *   <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
    *   <rootTarget> the earliest property in this <path> which contained an observers array    *
    */
    let observableValidation = {
        get(target, prop) {
            this.updateMarkers(target, prop);
            if (target[prop] && typeof target[prop] === 'object') {
                target[prop] = new Proxy(target[prop], observableValidation);
                return new Proxy(target[prop], observableValidation);
            } else {
                return target[prop];
            }
        },
        set(target, prop, value) {
            this.updateMarkers(target, prop);
            // user is attempting to update an entire observable field
            // so maintain the observers array
            target[prop] = this.path.length === 1 && prop !== 'length'
                ? Object.assign(value, { observers: target[prop].observers })
                : value;
            // don't send events on observer changes / magic length changes
            if(!this.path.includes('observers') && prop !== 'length') {
                this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
            }
            // reset the markers
            this.rootTarget = undefined;
            this.path.length = 0;
            return true;
        },
        updateMarkers(target, prop) {
            this.path.push(prop);
            this.rootTarget = this.path.length === 1 && prop !== 'length'
                ? target[prop]
                : target;
        },
        path: [],
        set rootTarget(target) {
            if(typeof target === 'undefined') {
                this._rootTarget = undefined;
            }
            else if(!this._rootTarget && target.hasOwnProperty('observers')) {
                this._rootTarget = Object.assign({}, target);
            }
        },
        get rootTarget() {
            return this._rootTarget;
        }
    };

    /*
    * create: Creates an object with keys governed by the fields array
    * The value at each key is an object with an observers array
    */
    function create(fields) {
        let observableModel = {};
        fields.forEach(f => observableModel[f] = { observers: [] });
        return new Proxy(observableModel, observableValidation);
    }

    return {create: create};
})();

Тогда тривиально создать наблюдаемую модель и зарегистрировать наблюдателей:

app.js

// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
    'profile',
    'availableGames'
]);

// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
    onEvent(field, newValue) {
        console.log(
            'handling profile event: \n\tfield: %s\n\tnewValue: %s',
            JSON.stringify(field),
            JSON.stringify(newValue));
    }
};

// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);

// make a change to profile - the observer prints:
// handling profile event:
//        field: ["profile"]
//        newValue: {"name":{"first":"jonny","last":"brooks"},"observers":[{}
// ]}
model.profile = {name: {first: 'jonny', last: 'brooks'}};

// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};

Надеюсь, это полезно!

Ответ 5

Я также опубликовал небольшую библиотеку GitHub, которая может перехватывать и проверять изменения на вложенных объектах с помощью Proxies. Подобно библиотеке Elliot, она также сохраняет сгенерированные прокси, поэтому их не нужно генерировать каждый раз.

Библиотека Эллиота великолепна, но то, как она сообщает об изменениях, когда задействованы функции, было не по душе. Самое большое различие между моим и его состоит в том, что изменения сообщаются атомарно с моей библиотекой. То есть, одно действие вызывает только один отчет об изменении, а не больше.