Ако харесвате програмиране, управлявано от данни, ще трябва да откриете промяна на данните. Има много начини за това. В тази статия ще представя две от тях.

Първият начин, който ми идва наум е Proxy. Ако знаете малко за Proxy. Може би е по-добре първо да прочетете това на MDN.

Използвайте прокси на обикновен обект

Повечето от нашите данни се съхраняват в обект. Лесно е да наблюдавате обикновен обект. Предполагаме, че промяната на данните е да зададете или изтриете данните. Така че добавяме капани на set и deleteProperty.

const obj = {};
const fn = () => console.log('we capture a change');
const proxy = new Proxy(obj, {
  set (target, property, value) {
    target[property] = value;
    fn();
    return true;
  },
  deleteProperty (target, property) {
    delete target[property];
    fn();
    return true;
  }
});
proxy.a = 2; // we capture a change
console.log(obj); // {a: 2}

Сега можете да промените obj до proxy, което ще направи същото и ще ви каже промяната. Ако трябва да наблюдавате други промени, можете да прочетете манипулатори в MDN.

Нашите данни обаче не винаги се съхраняват в обикновен обект, така че трябва да поддържаме наблюдение на обект на много нива.

Използвайте прокси на многослоен обект

Можем лесно да приложим това чрез рекурсия.

В примера по-горе ние задаваме прихващане на set и deleteProperty. Сега поставяме още един капан на get. Когато открием, че едно от нашите свойства съдържа обект, ние го заместваме с неговия прокси.

Също така трябва да помним дали имотът е проксиран. В противен случай може да предизвикаме ненужно задействане. Затова създавам празен обект mapStore.

  1. Ако mapStore[property] === undefined, това означава, че това свойство не е прокси.
  2. ако mapStore[property] === true, това означава, че това свойство е прокси.
  3. За останалите имотът не е проксиран. Но mapStore има своя прокси стойност.
function isObject (obj) {
  return typeof obj === 'object';
}
function deepProxy (obj, hook) {
  const mapStore = {};
  return new Proxy(obj, {
    get (target, property) {
      const value = target[property];
      // if this property has been proxied, just return
      if(mapStore[property] === true) return value;
      // if it's an non-proxied object, we return its proxy
      if(isObject(value)) {
        const proxyValue = mapStore[property] || deepProxy(value, hook);
        mapStore[property] = proxyValue;
        return proxyValue;
      }
      // else we just take a mark
      mapStore[property] = true;
      return value;
    },
    set (target, property, value) {
      const newVal = isObject(value)
        ? deepProxy(value, hook)
        : value;
      target[property] = newVal;
      mapStore[property] = true;
      hook();
      return true;
    },
    deleteProperty (target, propertty) {
      delete target[property];
      delete mapStore[property];
      hook();
      return true;
    }
  });
}
const obj = {
  foo: {
    a: 1
  }
};
const fn = () => console.log('we capture a change');
const proxy = deepProxy(obj, fn);
proxy.foo.a = 2; // we capture a change.

Много добре. Сега можем да открием промяна в обекта.

И така, какво ще кажете за масив?

Използвайте прокси в масив

Изглежда просто парче торта. Какво ще кажете просто да добавим isArray към манипулатора?

function isArray (arr) {
  return Array.isArray(arr);
}
...
    get (target, property) {
      const value = target[property];
      // if this property has been proxied, just return
      if(mapStore[property] === true) return value;
      // if it's an non-proxied object, we return its proxy
      if(isObject(value) || isArray(value)) {
        const proxyValue = mapStore[property] || deepProxy(value, hook);
        mapStore[property] = proxyValue;
        return proxyValue;
      }
      // else we just take a mark
      mapStore[property] = true;
      return value;
    },
    set (target, property, value) {
      const newVal = (isObject(value) || isArray(value))
        ? deepProxy(value, hook)
        : value;
      target[property] = newVal;
      mapStore[property] = true;
      hook();
      return true;
    },
...

Нека направим тест.

const obj = {
  foo: [1, 2, 3]
};
const fn = () => console.log('we caputre a change');
const proxy = deepProxy(obj, fn);
proxy.foo[1] = 4; // we capture a change
proxy.foo.unshift(2);
// we capture a change
// we capture a change
// we capture a change
// we capture a change

Какво стана?? Той улавя четири промени!!

Всичко е наред. Просто се успокой. Помислете внимателно. Какво направи unshfit?

Да приемем, че оригиналният масив е като таблицата по-долу.

0–1 , 1–4, 2–3

Когато изпълните unshift, той се движи по следния начин.

  1. Променете данните на първата позиция
  2. Преместете оригиналните първи данни на втората позиция
  3. Преместете оригиналните втори данни на трета позиция
  4. Преместете оригиналните трети данни на новата четвърта позиция

Така че всъщност получаваме 4 промени! Това е вярно.

Но в някои ситуации ги считаме само за една промяна. Трябва да направим някои промени тук.

Добавяме флаг, наречен arrayChanging за метода на масива. Когато изпълняваме метода, не задействаме никаква промяна. Ние задействаме само когато завършим изпълнението на метода.

И разбира се, ще отменим метода.

const arrayChangeMethod = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse'];
function isObject (obj) {
  return typeof obj === 'object';
}
function isArray (arr) {
  return Array.isArray(arr);
}
function deepProxy (obj, hook) {
  const mapStore = {};
  let arrayChanging = false;
  return new Proxy(obj, {
    get (target, property, receiver) {
      const value = target[property];
      if(isArray(target) && arrayChangeMethod.indexOf(property) > -1) {
        // we override the array's method
        return (...args) => {
          arrayChanging = true;
          value.bind(receiver)(...args);
          arrayChanging = false;
          hook();
        };
      }
      if(mapStore[property] === true) return value;
      if(isObject(value) || isArray(value)) {
        const proxyValue = mapStore[property] || deepProxy(value, hook);
        mapStore[property] = proxyValue;
        return proxyValue;
      }
      mapStore[property] = true;
      return value;
    },
    set (target, property, value) {
      const newVal = (isObject(value) || isArray(value))
        ? deepProxy(value, hook)
        : value;
      target[property] = newVal;
      mapStore[property] = true;
      if(!arrayChanging) hook();
      return true;
    },
    deleteProperty (target, propertty) {
      delete target[property];
      delete mapStore[property];
      if(!arrayChanging) hook();
      return true;
    }
  });
}

Нека направим теста отново!

const obj = {
  foo: [1, 2, 3]
};
const fn = () => console.log('we caputre a change');
const proxy = deepProxy(obj, fn);
proxy.foo[1] = 4; // we capture a change
proxy.foo.unshift(2); // we capture a change

Работи ~

Освен ако не пишете nodejs или вашият мениджър не е съгласен, че вашата страница поддържа само нов браузър, трябва да се погрижите за съвместимостта.

Нека да разгледаме „caniuse“.

Е, може да откриете, че браузърът, който трябва да поддържате, няма тази чудесна функция. И така, имаме ли някакви полипили? За съжаление, Proxy не може да се попълва с полиами.

Тук имаме нужда от резервна стратегия.

Резервен към Object.defineProperty

Object.defineProperty ни предложете пълномощия за добавяне на сетер за получаване на собственост. Чрез това можем да наблюдаваме промяната на данните. Ако знаете малко за Object.defineProperty, можете да прочетете това.

Лесно е да се изгради deepObserve по Object.defineProperty. Чрез това можем да открием всякакви промени в съществуващото свойство.

const {getOwnPropertyNames, getOwnPropertySymbols, defineProperty, getOwnPropertyDescriptor} = Object;
function isObject (obj) {
  return typeof obj === 'object';
}
function isArray (arr) {
  return Array.isArray(arr);
}
function isFunction (fn) {
  return typeof fn === 'function';
}
const getOwnKeys = isFunction(getOwnPropertySymbols)
  ? function (obj) {
    return getOwnPropertyNames(obj).concat(getOwnPropertySymbols(obj));
  }
  : getOwnPropertyNames;
function deepObserve (obj, hook) {
  const mapStore = {};
  if(isObject(obj) || isArray(obj)) {
    getOwnKeys(obj).forEach(key => {
      let value = obj[key];
      const desc = getOwnPropertyDescriptor(obj, key);
      if(desc && desc.configurable === false) return;
      defineProperty(obj, key, {
        get () {
          if(mapStore[key]) return value;
          if(isObject(value) || isArray(value)) {
            deepObserve(value, hook);
          }
          mapStore[key] = true;
          return value;
        },
        set (val) {
          if(isObject(val) || isArray(val)) deepObserve(val, hook);
          mapStore[key] = true;
          hook();
          return val;
        },
        enumerable: desc.enumerable,
        configurable: true
      })
    });
  }
  return obj;
}
const fn = () => console.log('we capture a change');
const obj = deepObserve({a: 1, b: [1, 2, 3]}, fn);
obj.a = 2; // we capture a change
obj.b[2] = 4; // we caputure a change

Замени метода на масива

Ако се опитате да използвате някакъв метод на масив като push, pop и т.н., може да се държи странно. Понякога улавя промяната, но понякога не. Това е така, защото ние наблюдаваме само част от собствеността.

Така че трябва да заменим метода на масива.

const arrayChangeMethod = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse'];
const {getOwnPropertyNames, getOwnPropertySymbols, defineProperty, getOwnPropertyDescriptor} = Object;
function isObject (obj) {
  return typeof obj === 'object';
}
function isArray (arr) {
  return Array.isArray(arr);
}
function isFunction (fn) {
  return typeof fn === 'function';
}
const getOwnKeys = isFunction(getOwnPropertySymbols)
  ? function (obj) {
    return getOwnPropertyNames(obj).concat(getOwnPropertySymbols(obj));
  }
  : getOwnPropertyNames;
function deepObserve (obj, hook) {
  const mapStore = {};
  let arrayChanging = false;
  function wrapProperty (key) {
    let value = obj[key];
    const desc = getOwnPropertyDescriptor(obj, key);
    if(desc && desc.configurable === false) return;
    defineProperty(obj, key, {
      get () {
        if(mapStore[key]) return value;
        if(isObject(value) || isArray(value)) {
          deepObserve(value, hook);
        }
        mapStore[key] = true;
        return value;
      },
      set (val) {
        if(isObject(val) || isArray(val)) deepObserve(val, hook);
        mapStore[key] = true;
        if(!arrayChanging) hook();
        return val;
      },
      enumerable: desc.enumerable,
      configurable: true
    });
  }
  if(isObject(obj) || isArray(obj)) {
    getOwnKeys(obj).forEach(key => wrapProperty(key));
  }
  if(isArray(obj)) {
    arrayChangeMethod.forEach(key => {
      const originFn = obj[key];
      defineProperty(obj, key, {
        value (...args) {
          const originLength = obj.length;
          arrayChanging = true;
          originFn.bind(obj)(...args);
          arrayChanging = false;
          if(obj.length > originLength) {
            const keys = new Array(obj.length - originLength)
            .fill(1)
            .map((value, index) => (index + originLength).toString());
            keys.forEach(key => wrapProperty(key));
          }
          hook();
        },
        enumerable: false,
        configurable: true,
        writable: true
      })
    })
  }
  return obj;
}
const fn = () => console.log('we capture a change');
const obj = deepObserve({a: 1, b: [1, 2, 3]}, fn);
obj.b.push(4); // we caputure a change

Сега поддържаме и метода на масива. Но не можем да открием промяната, когато delete или добавите ново свойство.

Е, това е една от причините, поради които не можем да попълним проксито.

Така че, ако искате да откриете промяна на delete или добавяне на свойство. Трябва да изпълните своя собствена функция, която също ще задейства куката. Пропускам го тук~

Можем ли да използваме часовник като декоратор?

Е, разбира се, че можем. Направих това в toxic-decorators. Можете да опитате.

Ако създавате само един екземпляр, можете да опитате това.

import {watch} from 'toxic-decorators';
class Foo {
  @watch(() => console.log('we capture a change'))
  bar = {
    a: 1
  }
}
const foo = new Foo();
foo.bar.a = 2; // we capture a change.