Найкраще пояснення реактивності JavaScript

Багато фреймворків JavaScript (наприклад, Angular, React і Vue) мають власні механізми реактивності. Розуміючи, що таке реактивність, і як вона працює, ви можете вдосконалити свої навички розробки та ефективніше використовувати рамки JavaScript. На відео та статті нижче ми будуємо той самий тип реактивності, який ви бачите у вихідному коді Vue.

Якщо ви дивитесь це відео замість того, щоб прочитати статтю, перегляньте наступне відео в серії, де обговорюється реактивність та проксі з Еваном Ви, творцем Vue.

Система реактивності

Система реактивності Vue може виглядати як магія, коли ви побачите її роботу вперше. Візьміть цей простий додаток Vue:

Якось Vue просто знає, що якщо ціна зміниться, вона повинна робити три речі:

  • Оновіть ціну на нашій веб-сторінці.
  • Перерахуйте вираз, що помножує кількість * кількість, і оновіть сторінку.
  • Викличте функцію totalPriceWithTax ще раз та оновіть сторінку.

Але зачекайте, я чую, що ви замислюєтеся, як Vue знає, що потрібно оновити, коли ціна змінюється, і як вона все відстежує?

Це не так, як правило, працює програмування JavaScript

Якщо вам це не очевидно, головна проблема, яку ми маємо вирішити, полягає в тому, що програмування зазвичай не працює таким чином. Наприклад, якщо я запускаю цей код:

Як ви думаєте, що це буде друкувати? Оскільки ми не використовуємо Vue, він надрукує 10.

У Vue ми хочемо, щоб сума оновлювалася щоразу, коли ціну чи кількість оновлювали. Ми хочемо:

На жаль, JavaScript є процедурним, нереактивним, тому це не працює в реальному житті. Для того, щоб зробити повну реакційну силу, ми повинні використовувати JavaScript для того, щоб все поводилося інакше.

Проблема

Нам потрібно зберегти, як обчислюємо загальну суму, щоб ми могли її повторно запустити, коли ціна чи кількість зміниться.

Рішення

По-перше, нам потрібен якийсь спосіб сказати нашій програмі: "Код, який я збираюся запустити, збережіть це, мені може знадобитися, щоб ви запустили його в інший час". Потім ми захочемо запустити код, і якщо ціна або змінні кількості оновлюються, запустіть збережений код ще раз.

Ми можемо зробити це, записавши функцію, щоб ми могли запустити її знову.

Зауважте, що ми зберігаємо анонімну функцію всередині цільової змінної, а потім викликаємо функцію запису. Використовуючи синтаксис стрілки ES6, я також міг записати це як:

Визначення запису просто:

Ми зберігаємо ціль (у нашому випадку {total = ціна * кількість}), щоб ми могли виконати її пізніше, можливо, за допомогою функції повтору, яка виконує всі записи, які ми записали.

Це проходить через усі анонімні функції, які ми зберігаємо всередині масиву зберігання та виконує кожну з них.

Тоді в нашому коді ми можемо просто:

Досить просто, правда? Ось код у повному обсязі, якщо вам потрібно перечитати і спробувати його зрозуміти ще раз. FYI, я кодую це особливим чином, якщо вам цікаво чому.

Проблема

Ми можемо продовжувати записувати цілі за потребою, але було б непогано мати більш надійне рішення, яке буде масштабуватись із нашим додатком. Можливо, клас, який піклується про підтримку списку цілей, які отримують сповіщення, коли нам вони потрібні для повторного запуску.

Рішення: Клас залежності

Один із способів ми можемо почати вирішувати цю проблему - це інкапсуляція такої поведінки у свій клас, клас залежності, який реалізує стандартний шаблон спостерігача програмування.

Отже, якщо ми створимо клас JavaScript для управління нашими залежностями (що ближче до того, як Vue обробляє речі), це може виглядати приблизно так:

Зауважте, замість пам’яті ми зараз зберігаємо свої анонімні функції в підписниках. Замість нашої функції запису, яку ми називаємо, тепер залежить, і ми використовуємо сповіщення замість відтворення. Щоб розпочати це:

Він все ще працює, і тепер наш код виглядає більш багаторазовим. Єдине, що все ще виглядає трохи дивно - це встановлення та біг мішені.

Проблема

В майбутньому ми будемо мати клас Dep для кожної змінної, і було б непогано скласти поведінку при створенні анонімних функцій, за якими потрібно стежити за оновленнями. Можливо, функція спостерігача може бути для того, щоб подбати про таку поведінку.

Тож замість дзвінка:

(це лише код зверху)

Замість цього ми можемо просто зателефонувати:

Рішення: Функція спостерігача

Всередині нашої функції Watcher ми можемо виконати кілька простих речей:

Як бачите, функція спостерігача бере аргумент myFunc, встановлює, що як наша глобальна цільова властивість, виклику dep.depend (), щоб додати нашу ціль як підписника, викликає цільову функцію та скидає ціль.

Тепер, коли ми запускаємо наступне:

Вам може бути цікаво, чому ми реалізували ціль як глобальну змінну, а не передавали її у свої функції там, де це було потрібно. Для цього є вагома причина, яка стане очевидною до кінця нашої статті.

Проблема

У нас є один клас Dep, але те, що ми дійсно хочемо, це те, що кожна з наших змінних має свій Dep. Дозвольте мені перемістити речі у властивості, перш ніж ми підемо далі.

Припустимо на хвилину, що кожен із наших властивостей (ціна та кількість) має свій внутрішній клас Dep.

Тепер, коли ми запускаємо:

Оскільки доступ до значення data.price (який він є), я хочу, щоб клас класу властивості Dep перемістив нашу анонімну функцію (зберігається в цілі) на свій передплатниковий масив (зателефонувавши dep.depend ()). Оскільки доступ до data.quantity є доступним, я також хочу, щоб клас властивості Dep відсунув цю анонімну функцію (зберігається в цілі) у свій передплатниковий масив.

Якщо у мене є інша анонімна функція, в якій доступ до даних data.price, я хочу, щоб вона перейшла до класу властивості Dep.

Коли я хочу зателефонувати dep.notify () абонентам? Я хочу, щоб вони дзвонили, коли встановлюється ціна. На кінець статті я хочу мати можливість зайти в консоль і зробити:

Нам потрібен певний спосіб приєднати до властивості даних (наприклад, ціна чи кількість), тож коли ми отримаємо доступ, ми можемо зберегти ціль у нашому абонентському масиві, а коли це буде змінено, запустити функції, що зберігаються у нашому абонентському масиві.

Рішення: Object.defineProperty ()

Нам потрібно дізнатися про функцію Object.defineProperty (), яка є звичайним ES5 JavaScript. Це дозволяє нам визначити функції властивостей і задач для властивості. Lemme покаже вам основне використання, перш ніж я покажу вам, як ми будемо використовувати його з нашим класом Dep.

Як бачите, він просто записує два рядки. Однак він фактично не отримує і не встановлює жодних значень, оскільки ми переоцінили функціональність. Давайте додамо його ще зараз. get () розраховує повернути значення, а set () все ще потребує оновлення значення, тому додамо змінну InternalValue, щоб зберігати наше поточне значення ціни.

Тепер, коли наші установки та налаштування працюють належним чином, що, на вашу думку, буде надруковано на консолі?

Тож у нас є спосіб отримувати сповіщення, коли ми отримуємо та встановлюємо значення. І з деякою рекурсією ми можемо виконати це для всіх елементів нашого масиву даних, правда?

FYI, Object.keys (дані) повертає масив ключів об'єкта.

Зараз у всіх є геттери та сетери, і ми це бачимо на консолі.

🛠 Об’єднання обох ідей

Коли фрагмент коду, як цей, запускається і отримує значення ціни, ми хочемо, щоб ціна запам'ятала цю анонімну функцію (ціль). Таким чином, якщо ціна буде змінена або встановлено нове значення, вона запустить цю функцію для повторного повторного використання, оскільки знає, що ця лінія залежить від неї. Тож ви можете думати про це так.

Get => Запам’ятайте цю анонімну функцію, ми її знову запустимо, коли наше значення зміниться.

Set => Запустіть збережену анонімну функцію, наше значення просто змінилося.

Або у випадку нашого класу Деп

Ціна доступна (отримати) => виклик dep.depend (), щоб зберегти поточну ціль

Набір цін => зателефонуйте dep.notify () про ціну, повторно виконуючи всі цілі

Давайте поєднаємо ці дві ідеї та переглянемо наш остаточний код.

А тепер подивіться, що відбувається в нашій консолі, коли ми будемо грати.

Саме те, на що ми сподівалися! І ціна, і кількість дійсно реагують! Наш загальний код повторно запускається щоразу, коли значення ціни або кількості оновлюються.

Ця ілюстрація з документів Vue повинна почати мати сенс вже зараз.

Чи бачите ви цей прекрасний фіолетовий круг Даних із геттерами та сетерами? Це повинно виглядати знайомо! Кожен компонентний екземпляр має примірник спостерігача (синього кольору), який збирає залежності від одержувачів (червона лінія). Коли пізніше викликається сеттер, він повідомляє спостерігача, що викликає повторне відображення компонента. Ось знову зображення з деякими власними примітками.

Так, чи не має це зараз набагато більше сенсу?

Очевидно, як Vue робить це під обкладинками складніше, але тепер ви знаєте основи.

Отже, чого ми дізналися?

  • Як створити клас Dep, який збирає залежність (залежність) і повторно запускає всі залежності (сповіщення).
  • Як створити спостерігача, щоб керувати кодом, який ми використовуємо, який, можливо, потрібно буде додати (цільовий) як залежність.
  • Як використовувати Object.defineProperty () для створення геттерів та сетерів.

Що далі?

Якщо вам сподобалося вчитися зі мною в цій статті, наступним кроком у вашому навчальному шляху є дізнання про реактивність з проксі. Однозначно перегляньте моє безкоштовне відео на цю тему на VueMastery.com, де я також розмовляю з Еваном Ви, творцем Vue.js.

Спочатку опубліковано на сайті www.vuemastery.com.