Динамика и иммутабельный flow

Наше приложение мы успешно сверстали. Но оно пока не делает ничего интересного. Исправим это.

Сделаем нашу статическую страницу динамической. Добавим таймер для виджета часов. Сделаем отложенную загрузку лотов по API. Реализуем обновление цен в реальном времени по примеру работы с WebSocket-уведомлениями.

Рассмотрим проблемы классического подхода прямого изменения DOM-элементов и перейдём на иммутабельные компоненты. Уберём лишние зависимости и переведём фронтенд на однонаправленный поток управления.

  • 00:00:56 Постановка задачи
  • 00:01:53 Добавление таймера
  • 00:02:39 Прямое изменение значения
  • 00:05:21 Усложнение виджета
  • 00:08:41 Проблема копипасты логики
  • 00:09:27 Унификация рендеринга
  • 00:12:02 Пересоздание компонента
  • 00:15:47 Перегенерация страницы
  • 00:18:23 Унификация процедуры render
  • 00:19:04 Способы модификации состояния
  • 00:21:49 Достоинства и недостатки подхода
  • 00:23:55 Отложенная загрузка лотов
  • 00:26:11 Получение данных по API
  • 00:29:53 Эмуляция API-клиента
  • 00:33:41 Обновление цен в реальном времени
  • 00:35:05 Эмулятор WebSocket-клиента
  • 00:38:19 Обзор Control Flow
  • 00:43:30 Проблемы сброса и производительности

Дальше займёмся решением проблем производительности и реализацией настоящей реактивности.

Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (23)
Konstantin

Спасибо, Дмитрий! Первый раз, первым оставил след )

Ответить
Yevhenii Lykholai

Очень понравился скрин каст. Классно, что теперь и по джс идет изложение в стиле Дмитрия. Надеюсь, что и про тайп скрипт когда-то пару слов скажете. Спасибо.

Ответить
fedot

Спасибо, за урок! Да, весьма интересный вопрос, только тайп скрипт может дать соблюдение строгости написания кода и крутые подсказки в шторме, но с другой стороны, возрастет сложность проекта и восприятие кода будет малость усложнено, тоже интересно по какому пути пойдет Дмитрий.

Ответить
Дмитрий Елисеев

Да, про TypeScript скажу.

Ответить
fedot

Спасибо.

Ответить
Danny

Спасибо! Очень нравятся ваши уроки!

Ответить
Sergei

21:08 я давно в JS не заглядывал. Синтаксический сахар синтаксического сахара. Комбо-вомбо :)

Ответить
Григорий

Дмитрий, спасибо, очень своевременно! Меня уже начал немного бесить фронтенд, но теперь я вижу и в нём логичные приёмы!

Ответить
Maksim

Вопрос не по теме урока, но не нашёл куда: доработайте пожалуйста сайт чтобы он запоминал какие видео я посмотрел а какие нет. Захожу не так часто - много времени уходит понять что я смотрел а что нет. Учитывая что я ещё часто не по порядку смотрю а только интересные темы.

Ответить
Дмитрий Елисеев

Добавил в планы по доработке. Спасибо!

Ответить
Aёct'ann

Суперское объяснение 👍 Спасибо за то, что Вы делаете!

Ответить
Marat

Я не думал что в этом плеере можно ускорить воспроизведение. Гораздо понятнее когда x2 )

Ответить
rodigy

айкон))

Ответить
S.Polessky

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

Пробовал переписывать - но это отнимает уйму времени, и крайне неудобно.

Спасибо!

P.S. В рамках данной серии уроков JSFiddle, думаю, будет предостаточно

Ответить
S.Polessky

Частично решено: в коммитах проекта на Github есть изменения :)

Ответить
Никита

А можете, пожалуйста, скинуть ссылочку на проект на Github?

Ответить
S.Polessky

В конце урока есть "Исходный код на GitHub" при наличии подписки Deworker есть. Боюсь, если продублирую - нарушу правила.

В Репозитории есть "105 commits"(комбинация CTRL+F в Firefox для поиска по тексту) - там есть исходный код уроков и шагов в них.

Ответить
S.Polessky

Унификация рендеринга - очень красиво.

Вы убрали прямое упоминания Виджета. Просто остался "сигнал" о изменении и вызов тонкого метода ререндеринга.

Ничего лишнего, всё просто и читаемо. Восхитительно!

Ответить
S.Polessky

Дмитрий, помогите разобраться с принципом действия Промисов, молю!

Представим поток выполнения:

=> [#1: displayMessage("Сейчас загрузим большое изображение)"
=> [#2: Promise(loadPicture)]
=> [#3: displayMessage(`Пока грузится: вот вам анекдот: ${anec}`)]
=>...
=> [#4: displayPicture() // from Promise Callback]

В шаге #2 Promise разблокировал поток вынеся загрузку "в фон" (WebAPI), дав возможность выполнятся шагу #3 в процессе загрузки изображения. А результат выполнения loadPicture поместит, когда загрузка будет выполнена (и стек свободный).

Верно? Это чисто для этого Промисы используются?

Ответить
Дмитрий Елисеев

Просто в JS код выполняется не один раз. У JS-движка есть так называемый Event Loop. Это бесконечный цикл, запускающих выполнение кода из очереди итерациями (тиками):

while(queue.waitForMessage()){
  queue.processNextMessage()
}

Простой синхронный скрипт выполняется весь в первом тике и очередь опустошается.

Но из нашего скрипта мы можем закинуть туда в очередь на будущее (как будто в фон) любой свой коллбэк через process.nextTick:

process.nextTick(function () {
  ...
});

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

Например, туда мы можем поместить проверку готовности чего-нибудь. И если готово, то выполнить onSuccess. А если ещё нет, то повторно подписаться на следующий тик:

function check(onSuccess) => {
  process.nextTick(() => {
    const ready = ...
    if (ready) {
      onSuccess()
    } else {
      check(onSuccess);
    }
  }
}

check(() => { console.log('Ready') })

console.log('Started')

В итоге у нас выведется Started, а check просто закинет коллбэк в очередь для следующей итерации Event Loop. И когда-нибудь потом в одной из итераций сработает if, вызовется onSuccess и выведется Ready.

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

А промис - это просто объект-контейнер, в котором хранятся наши три функции и статус c результатом. В конструктор передаётся наш основной код executor с любым асинхронным кодом, а потом в then и catch передаются коллбэки, которые он вызовет потом в случае успеха или ошибки.

И если бы мы сами писали Promise, то в более простом виде могли написать его с приёмом всех трёх функций в конструктор:

class Promise {
  constructor(executor, onSuccess, onError) {
    function resolve(result) {
       process.nextTick(() => onSuccess(result))
    }
    function reject(error) {
       process.nextTick(() => onError(error))
    }
    try {
      executor(resolve, reject)
    } catch (error) {
        reject(error)
    }
  })
}

И использовали бы его как:

new Promise(
  (resolve, reject) => { console.log('A'); resolve('B') },
  (result) => { console.log(result) },
  (error) => { console.log(error) }
)
console.log('C')

И это бы вывело результат как с настоящим промисом:

A
C
B

Мы у себя в промисе поместили вызов onSuccess в nextTick. Но реальный Event Loop сложнее и этапов там несколько. В нативном промисе в JS коллбэки из then и catch выполняются не в следующем тике, а на этапе микрозадач после текущего тика.

Таким образом промис - это обёртка, помогающая записывать асинхронный код с коллбэками в более удобном виде.

А в вашем примере не сам промис разблокирует поток, а просто его не блокирует асинхронная функция loadPicture. В каком-то тике она увидит, что файл до конца догружен и вызовет resolve, который дёрнет displayPicture. Если же loadPicture будет синхронной, то она выполнится сразу и промис ей никак не поможет.

Ответить
S.Polessky

Уточню: Сама асинхронная функция выполняется в "Отдельном потоке" (фон).
Event Loop следит - закончила ли выполнение асинхронная функция?
И если да - помещает её результат в Стак "Основного Потока".

Нарисовал иллюстрацию (порядок действий - пронумерован цифрами)

Обязанность Event Loop - следить закончила ли выполнение асинхрон. функция, и помещать её результат (коллбек) в Основной поток?

(И большое спасибо за подробное объяснение)

Ответить
Дмитрий Елисеев

Только при многопоточности код выполняется одновременно параллельно в "фоне" в других потоках.

А при асинхронности весь код выполняется последовательно в одном потоке чередуясь. В нём задача отправляется не в отдельный поток-фон, а просто добавляется в массив-очередь, чтобы Loop его запустил в этом же потоке после того, как завершится предыдущий код.

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Yandex
MailRu
GitHub
Google