Русский
Русский
English
Статистика
Реклама

Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 2

Часть первая

О генераторах

Генераторы - это новый вид функций, который появился в ES6. О них написано немало статей и приведено множество теоретических примеров. Что касается меня, то прояснить суть генераторов и способ их использования помогла книга You don't know JS, часть async & performance. Из всех книг по JS, которые я изучал, эта наиболее упакована полезной информацией без воды.

Представим, что генератор (функция в объявлении, которой есть *) - это некое электрическое устройство с дистанционным пультом управления. После создания и монтирования генератора (объявления функции) нужно его "крутануть" (выполнить эту функцию), чтобы он вращался на холостых оборотах и "запитал" пульт управления собой (при выполнении функции-генератора возвращает итератор). На этом пульте управления две кнопки: Пуск (вызвать первый раз метод next итератора) и Next (последующие вызовы метода next итератора). Далее с этим пультом управления можно носиться по всей электростанции (по нашему приложению) и когда понадобиться электрическая энергия (некие значения из функции-генератора) нажимать на пульте кнопку next (выполнять метод next() генератора). Генератор производит нужное количество электроэнергии (возвращает некое значение через yield) и опять переходит в холостой режим (функция-генератор ждёт следующего вызова next от итератора). Цикл продолжается, пока генератор может производить электричество (имеются операторы yield) или он не остановится (в функции-генераторе встретится return).

И во всей этой аналогии ключевой момент - это пульт управления (итератор). Его можно передавать в разные части приложения и в нужный момент "забирать" значения из генератора. Для полноты картины, на пульте управления можно добавить неограниченное количество кнопок для запуска генератора в определенных режимах (передача параметров в метод next(любые параметры) итератора), но для реализации хука достаточно и двух кнопок.

Вариант 4. Генератор без промисов

Этот вариант приводится для наглядности, т.к. в полную силу генераторы работают совместно с промисами (механизм async/await). Но этот вариант рабочий и имеет право на существование в определенных простых ситуациях.

Создаю в хуке переменную для хранения ссылки на итератор (ячейка для пульта управления генератором)

const iteratorRef = useRef(null);

Нужно изменить логику хука. При выполнении обработчика события будет запускаться генератор. В данном случае нет никакого обмена данными между генератором и кодом, выполняющим метод next() итератора (генератор просто делает один оборот при нажатии кнопки next). Выглядит это так:

const updateCounter = () => {  iteratorRef.current.next();};const checkImageLoading = (url) => {  const imageChecker = new Image();  imageChecker.addEventListener("load", updateCounter);  imageChecker.addEventListener("error", updateCounter);  imageChecker.src = url;};

При запуске генератор расставляет обработчики событий и затем запускает цикл по сборке ответов из этих обработчиков. Каждому обработчику был вручён пульт и было объяснено, что, как только придёт событие, нужно нажать кнопку next на пульте. Когда приходит событие, обработчик добросовестно жмёт на кнопку и тем самым "прокручивает генератор". В процессе оборота происходит dispatch нужного действия и генератор опять переходит в холостой режим, ожидая следующего сигнала от пульта. Ниже приведён код самого генератора:

function* main() {  for (let i = 0; i < imgArray.length; i++) {    checkImageLoading(imgArray[i].src);  }  for (let i = 0; i < imgArray.length; i++) {    yield true;    dispatch({      type: ACTIONS.SET_COUNTER,      data: stateRef.current.counter + stateRef.current.counterStep    });  }}

Конечно при монтировании хука нужно "крутануть" генератор, чтобы он запитал пульт (вернул итератор в iteratorRef. И после этого нажать кнопку Пуск (выполнить метод next итератора первый раз).

Ниже привожу полный исходный код хука с использованием генератора без промисов.

Исходный код хука Генератор без промисов
import { useReducer, useEffect, useLayoutEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const iteratorRef = useRef(null);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  const updateCounter = () => {    iteratorRef.current.next();  };  const checkImageLoading = (url) => {    const imageChecker = new Image();    imageChecker.addEventListener("load", updateCounter);    imageChecker.addEventListener("error", updateCounter);    imageChecker.src = url;  };  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1      });    }    function* main() {      for (let i = 0; i < imgArray.length; i++) {        checkImageLoading(imgArray[i].src);      }      for (let i = 0; i < imgArray.length; i++) {        yield true;        dispatch({          type: ACTIONS.SET_COUNTER,          data: stateRef.current.counter + stateRef.current.counterStep        });      }    }    iteratorRef.current = main();    iteratorRef.current.next();  }, []);  useLayoutEffect(() => {    stateRef.current = state;    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Но в полной мере мощь генераторов по управлению асинхронным кодом проявляется совместно с использованием промисов.

Вариант 5. Генератор с промисами

Как вы уже догадались генератор будет возвращать промис. Таким образом обработчикам событий не нужно будет вызывать метод next итератора (не нужно будет отдавать им пульт управления и давать инструкции по его использованию). Просто промисифицируем колбэк (мы сами будем знать когда отработал обработчик и без его ведома).

Код генератор изменится следующим образом:

const getImageLoading = async function* (imagesArray) {  for (const img of imagesArray) {    yield new Promise((resolve, reject) => {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => resolve(true));      imageChecker.addEventListener("error", () => resolve(true));      imageChecker.src = img.url;    });  }};

А вызывающий код будет выглядеть так:

for await (const response of getImageLoading(imgArray)) {  dispatch({    type: ACTIONS.SET_COUNTER,    data: stateRef.current.counter + stateRef.current.counterStep  });}

Основную работу по сравнению с предыдущим вариантом выполняет цикл for await ... of. Пульт управления генератором находится у него и он автоматически нажимает кнопку Пуск и Next.

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

Исходный код хука Генератор с промисами
import { useReducer, useEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    async function imageLoading() {      const imgArray = document.querySelectorAll("img");      if (imgArray.length > 0) {        dispatch({          type: ACTIONS.SET_COUNTER_STEP,          data: Math.floor(100 / imgArray.length) + 1        });          for await (const response of getImageLoading(imgArray)) {          dispatch({            type: ACTIONS.SET_COUNTER,            data: stateRef.current.counter + stateRef.current.counterStep          });        }      }    }    imageLoading();  }, []);  useEffect(() => {    stateRef.current = state;    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};const getImageLoading = async function* (imagesArray) {  for (const img of imagesArray) {    yield new Promise((resolve, reject) => {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => resolve(true));      imageChecker.addEventListener("error", () => resolve(true));      imageChecker.src = img.url;    });  }};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Итого:

В этой части статьи показано:

  • как использовать useRef для хранения и использования нужных значений на протяжении всего времени жизни компонента (некий аналог глобальных переменных для компонента)

  • как управлять потоком событий с помощью генераторов, но без использования промисов (с использованием колбэков)

  • как управлять потоком событий, обработчики которых промисифицированы, с помощью генераторов и цикла for await ... of

Ссылка напесочницу

Ссылка нарепозиторий

Продолжение следует... redux-saga...

Источник: habr.com
К списку статей
Опубликовано: 11.12.2020 12:12:55
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Reactjs

Хуки

React

Генераторы

Промисы

Promises

Generators

Итераторы

Await/async

Preloader

Прелоадер

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru