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

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

Часть 1. Кастомный хук

Часть 2. Генераторы

Redux-saga

Это middleware для управления сайд эффектами при работе с redux. В основе лежит механизм генераторов. Т.е. код ставится на паузу пока не будет выполнена определенная операция с эффектом - это объект с определенным типом и данными.

Можно представить себе redux-saga (middleware) как администратора камер хранения. В камеры хранения можно класть эффекты на неопределенный срок и забирать их оттуда, когда будет нужно. Есть такой посыльный put, который приходит к диспетчеру и просит положить в камеру хранения сообщение (эффект). Есть такой посыльный take, который приходит к диспетчеру и просит ему выдать сообщение с определенным типом (эффект). Диспетчер, по просьбе take, смотрит все камеры хранения и если этих данных нет, то take остаётся с диспетчером и ждёт, пока put не принесёт данные с нужным для take типом. Существуют разные виды таких посыльных (takeEvery и т.д.).

Основная идея камер хранения - это "развести" во времени отправителя и получателя (некий аналог асинхронных обработки).

Redux-saga - это просто инструмент, а вот главным тут является тот, кто посылает всех этих посыльных и обрабатывает данные, которые они приносят. Этим "кто-то" является функция-генератор (назову её пассажир), которая в справке называется saga и передаётся при запуске middleware. Запустить middleware можно двумя способами: с помощью middleware.run(saga, ...args) и runSaga(options, saga, ...args). Saga - это функция-генератор с логикой обработки эффектов.

Меня заинтересовала возможность использования redux-saga для обработки внешних событий без redux. Рассмотрю метод runSaga(...) подробнее:

runSaga(options, saga, ...args)

saga - это метод, в котором будет выполняться логика;

args - аргументы, которые будут переданы в saga;

options - объект, который "настраивает" работу redux-saga. Для данного хука использую всего три настройки:

channel - канал, из которого будут поступать внешние события;

dispatch - это метод, который при возникновении события, должен послать redux-saga эффект с помощью put.

getState - функция, которая используется для выборки данных из state, с которым используется redux-saga. В случае с хуком это будет локальный state.

Вариант 6. Redux-saga как канал обработки внешних сообщений

Логика работы хука с saga будет такова. Создаётся канал channel (камеры хранения) для хранения эффектов в процессе работы redux-saga. Создаётся канал, в который будут поступать внешние события от изображений - eventsChannel. Это два разных канала! Вернусь к аналогии с камерами хранения.

Создаются камеры хранения (channel), которым потом будет назначен администратор (redux-saga)

const sagaChannelRef = useRef(stdChannel());

При запуске runSaga() redux-saga назначается администратором созданных камер хранения.

runSaga(  {    channel: sagaChannelRef.current,    dispatch: () => {},    getState: () => {},  },  saga);

Камеры хранения созданы (channel), администратор назначен (redux-saga) и далее этим всем начинает пользоваться пассажир (происходит запуск функции-генератора saga)

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

const eventsChannel = yield call(getImageLoadingSagas, imgArray);
function getImageLoadingSagas(imagesArray) {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.url;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {    };  }, buffers.expanding(10));}

Т.е. пассажир (функция-генератор saga) просит диспетчера (redux-saga) принимать сообщения не только от посыльного put, но и от другого источника (eventsChannel). Сообщения от этого источника (eventChannel) у диспетчера (redux-saga) будет забирать, специально выделенный для этого, посыльный take, который стоит рядом с диспетчером и ждёт от него сообщения.

yield take(eventsChannel);

Как только диспетчеру (redux-saga) приходит сообщение от eventChannel, он тут же отдает его take, который возвращает сообщение пассажиру (функции-генератору saga). Сам take остаётся рядом с пассажиром и ждёт от него указаний.

Пассажир (функция-генератор saga) отдает это сообщение на обработку другому пассажиру (функции-генератору putCounter) с помощью call(). Это означает, что пассажир saga (функция-генератор saga) будет ожидать, пока пассажир putCounter (функция-генератор putCounter) не освободится (т.е. saga блокируется, пока не отработает функция putCounter).

yield call(putCounter);
function* putCounter() {  dispatch({    type: ACTIONS.SET_COUNTER,    data: stateRef.current.counter + stateRef.current.counterStep,  });  yield take((action) => {    return action.type === "STATE_UPDATED";  });}

Чем занимается пассажир putCounter (функция-генератор putCounter). Диспатчит действие состояния хука и затем посылает посыльного take к диспетчеру (redux-saga) за сообщением с типом STATE_UPDATED и ждёт этого посыльного.

В этом месте остановимся и ещё раз опишем получившийся хоровод (сделаем срез на этот момент).

Посыльный take(eventChannel) стоит (ожидает пока не выполнится итерация цикла в функции-генераторе saga) рядом с пассажиром saga (функцией-генератором saga). Пассажир saga (функция-генератор saga) ожидает пока пассажир putCounter (функция-генератор putCounter) не освободится. Пассажир putCounter (функция-генератор putCounter), в свою очередь, ждёт посыльного take, который стоит рядом с диспетчером (redux-saga) и ждёт посыльного put, который должен принести сообщение с типом STATE_UPDATED. Короче "Дом, который построил Джек".

Таким образом весь хоровод "застыл" в ожидании одного-единственного сообщения STATE_UPDATED. Кстати, в канале eventChannel могут возникать события во время этого застывшего состояния. Если не использовать буфер с каналом eventChannel, то эти события останутся незамеченными для нашего диспетчера (redux-saga). Но буфер у нас есть, поэтому в это время в тамбуре перед камерами хранения (буфере) толпятся сообщения от eventChannel.

И этого посыльного put отправляет хук useEffect

useEffect(() => {...    sagaChannelRef.current.put({ type: "STATE_UPDATED" }); ...}, [state]);

Посыльный put приносит сообщение STATE_UPDATED диспетчеру (redux-saga).

Диспетчер (redux-saga) отдаёт его take, которого прислал пассажир putCounter.

Пассажир putCounter сообщает пассажиру saga, что он освободился.

Пассажир saga, отправляет посыльного take за следующим сообщением от eventChannel

Take забирает следующее сообщение у диспетчера, который взял его из буфера.

Круг замкнулся.

Исходный код хука с redux-saga в качестве канала обработки событий
import { useReducer, useEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";import { runSaga, eventChannel, stdChannel, buffers, END } from "redux-saga";import { call, take } from "redux-saga/effects";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const sagaChannelRef = useRef(stdChannel());  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1,      });      function* putCounter() {        dispatch({          type: ACTIONS.SET_COUNTER,          data: stateRef.current.counter + stateRef.current.counterStep,        });        yield take((action) => {          return action.type === "STATE_UPDATED";        });      }      function* saga() {        const eventsChannel = yield call(getImageLoadingSagas, imgArray);        try {          while (true) {            yield take(eventsChannel);            yield call(putCounter);          }        } finally {          //channel closed        }      }      runSaga(        {          channel: sagaChannelRef.current,          dispatch: () => {},          getState: () => {},        },        saga      );    }  }, []);  useEffect(() => {    stateRef.current = state;    if (stateRef.current.counterStep != 0 && stateRef.current.counter != 0) {      sagaChannelRef.current.put({ type: "STATE_UPDATED" });    }    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};function getImageLoadingSagas(imagesArray) {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.url;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {          };  }, buffers.expanding(10));}const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Я был трезвый, когда это писал. Чтобы прочитать, пить тоже не нужно.

Вариант 7. Redux-saga + useReducer = useReducerAndSaga

В варианте 6 сага использовалась исключительно как менеджер событий. Мне хотелось подключить его к управлению state хука. Погуглив нашёл такой вариант хука useReducerAndSaga

Описывать на примере камер хранения не буду, просто приведу исходный код

Исходный код useReducerAndSaga.js
import { useReducer, useEffect, useRef } from "react";import { runSaga, stdChannel, buffers } from "redux-saga";export function useReducerAndSaga(reducer, state0, saga, sagaOptions) {  const [state, reactDispatch] = useReducer(reducer, state0);  const sagaEnv = useRef({ state: state0, pendingActions: [] });  function dispatch(action) {    console.log("useReducerAndSaga: react dispatch", action);    reactDispatch(action);    console.log("useReducerAndSaga: post react dispatch", action);    // dispatch to sagas is done in the commit phase    sagaEnv.current.pendingActions.push(action);  }  useEffect(() => {    console.log("useReducerAndSaga: update saga state");    // sync with react state, *should* be safe since we're in commit phase    sagaEnv.current.state = state;    const pendingActions = sagaEnv.current.pendingActions;    // flush any pending actions, since we're in commit phase, reducer    // should've handled all those actions    if (pendingActions.length > 0) {      sagaEnv.current.pendingActions = [];      console.log("useReducerAndSaga: flush saga actions");      pendingActions.forEach((action) => sagaEnv.current.channel.put(action));      sagaEnv.current.channel.put({ type: "REACT_STATE_READY", state });    }  });  // This is a one-time effect that starts the root saga  useEffect(() => {    sagaEnv.current.channel = stdChannel();    const task = runSaga(      {        ...sagaOptions,        channel: sagaEnv.current.channel,        dispatch,        getState: () => {          return sagaEnv.current.state;        }      },      saga    );    return () => task.cancel();  }, []);  return [state, dispatch];}

Все генераторы были выдесены в отдельный файл sagas.js

Исходный код sagas.js
import { eventChannel, buffers } from "redux-saga";import { call, select, take, put } from "redux-saga/effects";import { ACTIONS, getCounterStep, getCounter, END } from "./state";export const getImageLoadingSagas = (imagesArray) => {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();            imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.src;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {};  }, buffers.fixed(20));};function* putCounter() {  const currentCounter = yield select(getCounter);  const counterStep = yield select(getCounterStep);  yield put({ type: ACTIONS.SET_COUNTER, data: currentCounter + counterStep });  yield take((action) => {    return action.type === "REACT_STATE_READY";  });}function* launchLoadingEvents(imgArray) {  const eventsChannel = yield call(getImageLoadingSagas, imgArray);  while (true) {    yield take(eventsChannel);    yield call(putCounter);  }}export function* saga() {  while (true) {    const { data } = yield take(ACTIONS.SET_IMAGES);    yield call(launchLoadingEvents, data);  }}

Немножко изменился state. Был добавлен action SET_IMAGES и селекторы для counter и counterStep

Исходный код state.js
const SET_COUNTER = "SET_COUNTER";const SET_COUNTER_STEP = "SET_COUNTER_STEP";const SET_IMAGES = "SET_IMAGES";export const initialState = {  counter: 0,  counterStep: 0,  images: [],};export const reducer = (state, action) => {  switch (action.type) {    case SET_IMAGES:      return { ...state, images: action.data };    case SET_COUNTER:      return { ...state, counter: action.data };    case SET_COUNTER_STEP:      return { ...state, counterStep: action.data };    default:      throw new Error("This action is not applicable to this component.");  }};export const ACTIONS = {  SET_COUNTER,  SET_COUNTER_STEP,  SET_IMAGES,};export const getCounterStep = (state) => state.counterStep;export const getCounter = (state) => state.counter;

Благодаря этому рефакторингу, код хука usePreloader стал компактным.

Исходный код usePreloader.js
import { useEffect } from "react";import { reducer, initialState, ACTIONS } from "./state";import { useReducerAndSaga } from "./useReducerAndSaga";import { saga } from "./sagas";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducerAndSaga(reducer, initialState, saga);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1,      });      dispatch({        type: ACTIONS.SET_IMAGES,        data: imgArray,      });    }  }, []);  useEffect(() => {    if (counterEl) {      state.counter < 100        ? (counterEl.innerHTML = `${state.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state.counter]);  return;};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Итого

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

  • что такое redux-saga

  • как использовать redux-saga без redux

  • как использовать redux-saga для управления состоянием хука

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

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

Продолжение следует... RxJS...

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

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

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

Javascript

Reactjs

Redux-saga

Put

Take

Channel

Eventchannel

Hook

Категории

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

  • Имя: Макс
    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