?Вопрос отслеживания изменения переменных (или свойств объектов) в javascript возникает, как правило, у разработчиков после какого-то времени проведенного за написанием кода. В тот самый момент, когда вопрос «а как это сделать?» сменяется вопросом "а как это сделать более оптимально?".

Согласитесь, «связать» свойство, отвечающее за какое-либо динамическое значение вашего web-приложения, с функцией обновления DOM дерева, или же с методами строящими расчеты, что бы в дальнейшем работать лишь с моделью, не думая о последующих необходимых манипуляциях — не только удобно, но и красиво с точки зрения архитектуры.

Если вы строите свое приложение на библиотеке или фреймворке, где существует реализация данного вопроса (react предлагает к использованию метод setstate, vue дает в распоряжение пользователя свою систему реактивности) то здесь все и так понятно.

Однако методы реализации поставленной задачи на чистом javascript «из коробки» несколько сложнее «простого» и не всегда удобны к использованию (Object.defineProperty(), Proxy). Суперлегковестная библиотека floss-js (< 1кб) призвана предоставить функционал отслеживания изменения свойств и вызова методов-обработчиков.Работая под капотом с Object.defineProperty библиотека хранит реальные значения в отдельном специальном хранилище данных, проксируя их через пользовательские объявленные свойства и отслеживая события обращения (возвращает значение из хранилища) и изменения (перезаписывает значение в хранилище и вызывает пользовательскую функцию обработчик).

Проще говоря: floss-js (github) следит за переменными и свойствами объекта и выполняет пользовательский код по факту их изменения.

image

Установка


Добавить библиотеку в проект очень легко.

  • Можно воспользоваться пакетным менеджером npm:
    npm install floss-js --save
  • или yarn:
    yarn add floss-js
  • или просто добавить скрипт в html документ:
    <script src="FLOSS.min.js"></script>

Создание события


Предположим есть небольшое web приложение, реализующее какой-то функционал. Какой именно для нас значения не имеет. В приложении существует переменная state которая содержит в себе строку, сообщающую о текущем состоянии приложения (например: loading, expectation of the user, user writing..., saving..., unexpected error и т.д.); значение переменной может меняться как ввиду данных поступающих с сервера, так и из-за пользовательских манипуляций.

В DOM существует контейнер выводящий уведомления для пользователя:


<div id="state"></div>

Мы уже подключили FLOSS.min.js и теперь нам нужно повесить обработчик: при изменении значения state должна вызываться функция обновляющая DOM дерево.


/* функция обновляющая состояние приложения в DOM */
let updateState = (state) => {
  document.querySelector('#state').innerHTML = state;
}

Далее вешаем сам обработчик:


/* обработчик создается зарезервированной функций FLOSS */
FLOSS({
   name: 'state',
   value: 'Waiting for status',
   action: (newState) => {
      updateState(newState);
   }
});

Рассмотрим более подробно: name — имя переменной или свойства, за изменениями которого необходимо следить. Если переменной не существует, она будет создана глобально, как свойство объекта window. Если необходимо следить за свойством объекта, то сам объект также необходимо передать в свойство bind функции FLOSS. value — default значение присваиваемое переменной. И, наконец, action — функция обработчик, в которую передается новое значение переменной, после изменения. Впервые action срабатывает после создания FLOSS обработчика. Поэтому в данном примере функция updateState — отработает сразу и выведет пользователю «Waiting for status». Если первый вызов action необходимо отложить до факта изменения отслеживаемой переменной, то можно указать дополнительный параметр defer: true.

Реализовав данный пример, можно прямо в консоли задать state другое значение. FLOSS обработчик тут же среагирует и вызовет метод обновляющий DOM.

Все просто, не правда ли?

Давайте рассмотрим еще один, более сложный, пример использования микробиблиотеки.
Ситуация схожа: необходимо отслеживать изменения состояния приложения. Однако само состояние теперь храниться в специальном объекте и текст уведомление является вычисляемым свойством.


let server = {
        state: {
            200: 'OK',
            201: 'Created',
            203: 'Non-Authoritative Information',
            526: 'Invalid SSL Certificate'
        },
        logs: '...',
        status: '200',
        notification: function(){
            return `${this.state[this.status]}, status: ${this.status}`
        }
    }

Нас интересует свойство notificaion. Однако оно является функцией, формирующей уведомление для пользователя. Соответственно отслеживать мы будем свойство status.


FLOSS({
            name: 'status',
            value: server.status, /*  параметр value определяем из исходного объекта */
            action: function(){
                updateState(server.notification());
            },
            defer: false,
            bind: server /* объект, свойство которого необходимо отслеживать */
        });

Естественно, использование следящих за состоянием переменных методов целесообразно не всегда. Зачастую в данном приеме необходимости нет, а если и есть, то всегда можно реализовать собственный узконаправленный метод, принимающий на вход новое значение, мутирующий необходимое свойство, а также вызывающий все необходимые функции обработчики. Однако если ваше детище балансирует между небольшим приложением и приложением требующем фундамента вроде REACT — c помощью FLOSS можно сделать код более минималистичным и структурированным.

Спасибо за внимание.

Комментарии (22)


  1. mwizard
    01.08.2018 23:14
    +1

    1. Хардкод `window` всюду. 2018 год, кому нужен node.js?
    2. Хардкод `window.flossGlobalStorageV1_0` вместо Symbol() или приватного свойства. Хотя, если бы это было модулем (см. пп. №3), кэш значений мог бы быть просто переменной в локальном scope модуля.
    3. `window.FLOSS = ...` вместо системы модулей, хоть какой-нибудь, пусть даже CommonJS, не говоря уже о ES6 `export function`.
    4. Ваша поделка пытается переопределить дескриптор свойства целевого объекта. Как быть, если это non-configurable?
    5. Как быть, если вы уже похерили чужой геттер и чужой сеттер?

    В целом, код ужасен. Не пишите так, пожалуйста.

    UPD: Ах да, ожидаемо ни единого теста, который бы позволил словить все вышеперечисленное.
    UPD2: На Proxy тоже чихнет.


    1. mwizard
      01.08.2018 23:20

      Также открытый вопрос о целесообразности подобной штуковины и внесения ею трудноотлаживамого поведения. Даже в вашем примере notification() использует поле status, поэтому отслеживать надо непосредственно status. Как быть, если нужная функция работает с массивом? Как быть, если нужная функция вызывает метод вложенного объекта? Как быть, если реализации этих объектов скрыты за интерфейсами, как и положено, а изменяемые аттрибуты в лучшем случае являются геттерами?


    1. DmitryTitov Автор
      01.08.2018 23:49

      Спасибо за критику, многие из указанных недочетов действительно есть смысл исправлять. А что за приватные методы в js?


      1. mwizard
        01.08.2018 23:54

        https://github.com/tc39/proposal-class-fields, stage 3 proposal. Это финальная стадия, дальше только включение в стандарт.


        Если вкратце, то можно делать вот так:


        class Foobar {
            #verySecret = 123;
        
            constructor(secret) {
                this.#verySecret = secret;
            }
        
            incSecret() {
                return this.#verySecret++;
            }
        }


        1. gnaeus
          02.08.2018 10:25

          Интересно, чем им не понравились ключевые слова private | public | protected из TypeScript? Опять сломали совместимость :(


          Особенно если учесть, что эти слова уже являются ключевыми в ES2015. Видимо, когда-то хотели использовать их как раз для этих целей. А теперь получается, что ключевое слово private есть, а использовать его никогда не будут.


          1. ThisMan
            02.08.2018 10:41

            Согласен, # смотрится как-то инородно и вообще не по стилю js. private отлично бы вписался, на фоне того, что уже существует тот же super


            1. justboris
              02.08.2018 20:26

              А что такое «стиль js»? В чем он заключается?


          1. ThisMan
            02.08.2018 10:44
            +1

            https://github.com/tc39/proposal-class-fields/issues/100 тут вот тоже есть куча недовольных


            1. gnaeus
              02.08.2018 11:10

              Спасибо! Присоединился к ним.


              Попутно сломали object destructuring:


              class Test {
                #x = 0;
                constructor() {
                  const { #x } = this; // !!!
                }
              }

              Хочется уже сказать "горшочек не вари"!


          1. mwizard
            02.08.2018 11:29

            Похоже, причина как раз в том, что приватные поля, с целью сохранения обратной совместимости, это полностью отдельный неймспейс. Т.е. пришлось бы писать что-то типа this.(private foo) = 10;, а это еще хуже, чем this.#foo.


            Плюс protected, как такового, нет, и я не вижу каких-то очевидных способов его реализовать.


            В принципе, TypeScript мог бы компилировать private foo в #foo, хотя тут все тоже не совсем очевидно, т.к. возникает вопрос интероперабельности с обычными js-модулями через .d.ts.


            1. gnaeus
              02.08.2018 14:22

              А в чем нарушение обратной совместимости?


              this.(private foo) = 10; — это объявление приватного поля или что? Если объявление, то не проще ли требовать заранее объявления всех приватных полей в классе без инициализации? Как сейчас это делается в TypeScript:


              class Test {
                private foo;
                private bar;
              }


              1. mwizard
                02.08.2018 14:24

                Нет, использование. Объявление сейчас и есть "заранее" — нужно написать


                class Test {
                  #foo;
                }

                и, получается, динамических private нету. В принципе, вы правы, я просто пытаюсь ретроспективно вывести причины, почему комитет отбросил идею с private.


      1. mwizard
        01.08.2018 23:59

        О, и мимо меня прошел аналогичный stage 3 proposal https://github.com/tc39/proposal-private-methods:


        class Foobar {
            #privateMethod() {
                return 'so private! very secret!';
            }
        
            frob() {
                return this.#privateMethod();
            }
        }


  1. Iqorek
    01.08.2018 23:35
    +1

    Не понятно зачем вообще нужен flossGlobalStorageV1_0, все можно сократить до:

    const watch = ({
      target = window,
      propertyName,
      onChange,
      value = target[propertyName]
    }) => {
      Object.defineProperty(target, propertyName, {
        get: () => {
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            onChange(value)
          }
        }
      })
    }
    ...
    watch({
      target: obj,
      propertyName: 'status',
      onChange: value => {
        console.log('updated:', value)
      }
    })
    

    jsfiddle.net/jsbot/rka583Lq


    1. DmitryTitov Автор
      02.08.2018 12:54

      да, но для дальнейшего развития библиотеки мне нужны все значения в отдельном хранилище. Другое дело, что способ реализации хранилища требует доработок =)


  1. bromzh
    02.08.2018 02:16

    / только при фактическом изменении, но не при каждом вызове сеттера /
    if (value !== window.flossGlobalStorageV1_0[context.name][name]){

    Нууу, почти


    > [1, 2] === [1, 2]
    < false
    > { a: 1 } === { a: 1 }
    < false


  1. jehy
    02.08.2018 09:54

    Так с использованием Proxy это точно так же пишется в несколько строчек сеттера, что вас не устроило?


    1. DmitryTitov Автор
      02.08.2018 11:51

      ES6


      1. jehy
        02.08.2018 12:06

        Ну так вроде ж полифилл есть. И то он нужен будет для полутора инвалидов… Хотя я пишу бэк на Node.JS, далёк от таких проблем и могу ошибаться.


  1. ThisMan
    02.08.2018 10:50

    Вы после этого вопроса решили написать эту статью?


    1. DmitryTitov Автор
      02.08.2018 11:50

      Решил написать, что бы узнать мнения 'дедкшек' которые уже 70 лет занимаются клиентской разработкой)) ну и понять что необходимо править и дорабатывать).


  1. DmitryTitov Автор
    02.08.2018 12:15

    В комментариях много толковой критики, так что советую немного повременить с манипуляциями, пока я не внесу ряд изменений =)