Галоўная / Артыкулы

Скрытыя спады запыткаў: падбір асінхроннага коду да рэальнай графікі залежнасцяў

Выучыце, як выявляць последовальныя асэнты, якія ствараюць фальшывыя залежнасці, як выправляць іх за дапамою Promise.all, а таксама як пераструктураваць панель керування React так, каб кожны компонент запрашаў толькі тое, што яму трэба.

Выдавец: REACTAPP.TOP5 хв чытання1238 слоў

Крыніцы: Promises Make Async Code Look More Sequential Than It Really Is

async/await дапамагае асінхроннаму коду чытацца зверху вярх, ал такая чытаемасць караецца: кожны await таямніча прызнае, што наступны рядок залежыць ад пярэднего. Калі гэта прызнанне некароткае, створаецца «водоспад» запытак, які выглядае абсалютна чыста пад пераглядам, але прыводзіць да затрымкі толькі ў вікні сеті. Ёсць прыклад загрузча панелі керування, у яком гэта проблема спачатку рашылася за дапамогою Promise.all, а потым шляхом раздзелення компонента, так што правильны порядак стварыўся сама сабою.

Загрузч панелі керування, який робіць усё

Рэдагувальнікі панелей керування выглядаюць проста, але адзін компонент часта выкананы загружае панель керування, запрашвае кожны віджэт, загружае ўсі параметры і фільтры кожнага віджэта, выявляе його тип, запрашвае даны і нарэшце відобразае інтерфейс. Ёсць спроставаная версія часткі такога компонента, якая адпаведае за загрузку даных:

useEffect(() => {
  const loadDashboard = async () => {
    const dashboard = await getDashboard(id);

    const widgets = [];

    for (const widgetId of dashboard.widgetIds) {
      const options = await getWidgetOptions(widgetId);
      const filters = await getWidgetFilters(widgetId);
      const widget = await getWidget(widgetId);

      const data = await getWidgetData(
        widgetId,
        filters,
        options,
      );

      widgets.push({
        widget,
        options,
        filters,
        data,
      });
    }
    setWidgets(widgets);
  };

  loadDashboard();
}, [id]);

Кроме таго, што гэты код ў формате імператыўных запаведзей усередзінны ў эфект, ён таксама выклікае некалькі хыбных прыпускаў.

Два аднальныя проблемы ў однам цікле

Першая проблема легка зрозумела: цікл for...of обрабоцвае кожны віджэт по аднаму, хоця віджэты є незалежнымі. Promise.all яўляецца очвідным рашэнням, але ён спрацоўвае толькі часткова.

Другая проблема є болей суптыльнай. Усередзінны ў цікл код спачатку запрашоўвае опцыі і чакае, пасля чаго запрашоўвае фільтры і чакае, потым запрашоўвае сам віджэт і чакае, толькі пасля чаго запрашоўвае даны. Ціясныя оператары await описваюць залежнасці, якіх на самай працэ няма. Опцыі не патрабуюць фільтраў, фільтры не патрабуюць віджэта, а запрашанне даных узагалі не выкарыстоўвае віджэт. Рэальная графіка залежнасцяў для аднаго віджэта выглядае так:

widgetId → options ─┐
         → filters ─┴→ data
         → widget

На самай працэ код реалізуе простую ланцюговую структуру:

widgetId → options → filters → widget → data

Для панелі керування з N віджетаміяслявнычальны элемент выконвае прыблызна 4N парадных запускаў адно за іншым. Вікна сеті чытра паказваюць гэта: запиты выходзяць адно за іншым, па аднам віджэту.

Першая парадкавка: кодуванне рэальнай структуры з адной Promise.all

Швайная парадкавка — гэта прымус коду следаваць рэальнай структуре. Віджеты обрабоўваюцца паралельна, і ў кожным віджэты трое незалежных запитоў пачынаюцься адночасова. Лячыцца толькі запит на даны, бо ёму сапраўды патрэбны параметры і фільтры:

useEffect(() => {
  const loadDashboard = async () => {
    const dashboard = await getDashboard(id);

    const widgets = await Promise.all(
      dashboard.widgetIds.map(async (widgetId) => {
        const [options, filters, widget] = await Promise.all([
          getWidgetOptions(widgetId),
          getWidgetFilters(widgetId),
          getWidget(widgetId),
        ]);

        const data = await getWidgetData(
          widgetId,
          filters,
          options,
        );

        return {
          widget,
          options,
          filters,
          data,
        };
      }),
    );
    setWidgets(widgets);
  };

  loadDashboard();
}, [id]);

Критычны шлях зменшваецца з ланцуга чатырох запитоў на кожны віджэт, умножанага на колькасць віджэтаў, да запиту панелі керування плюс спадзейшы з трох паралельных запісоў плюс запит на даны.

Есць два моманты, які варта адзначыць пры апрадке гэтага. Promise.all адхроюецца як толькі хоць яка-небудзь запытка парадуе, таму адна несправная дзялянка выцірае весь панель керування; Promise.allSettled або обработка паказанняяў для кожнай дзялянкі дазволяе застаць іншыя элементы виднымі. Кроме таго, гэты метод не мае функцыі чысткі, таму якщо id быстра зменяецца, медленная адпаведзь для старага панелі керування можа перазапісаць новы. Флаг для ігноравання або AbortController у функцыі чысткі запобегае гэтаму.

Лепшая аднаўка: нехай компаненты самі керуюць сваімі трэбаваннямі

Нават пасля аднаўкі адны компанент все равно керуе кожной запыткай і кожным модэлем, якія патрэбны панелі керування. Болей стойкая змена — это раздзеліць інтэрфейс за адпаведальнасцямі і дазволіць кожнай часткі запрашваць свае сабліжныя даны через хукі дадзеных:

const DashboardsPage = () => {
  const dashboards = useDashboards();

  return <Navigate to={`/dashboards/${dashboards[0].id}`} />;
};

const Dashboard = () => {
  const { id } = useParams();
  const dashboard = useDashboard(id);

  return dashboard.widgetIds.map((widgetId) => (
    <WidgetSkeleton key={widgetId} widgetId={widgetId} />
  ));
};

const WidgetSkeleton = ({ widgetId }: { widgetId: string }) => {
  const options = useWidgetOptions(widgetId);
  const filters = useWidgetFilters(widgetId);

  return (
    <>
      <WidgetHeader options={options} filters={filters} />
      <Widget
        widgetId={widgetId}
        options={options}
        filters={filters}
      />
    </>
  );
};

DashboardsPage пераправляе на першы дашборд (у рэальным кодзе трэба захавацца ад порожнім спискам). Dashboard ведае толькі ідэнтыфікаторы віджэтаў і атрыбутуе кожнам з іх WidgetSkeleton на адпаведны ключ. WidgetSkeleton патрэбуюць настройкі і фільтры для заголовка і самага віджэта, таму гэтыя элементы ёсць проста двума вакалідатамі адной і той жа часткі інтэрфейсу, без павярнення ў порядку.

Апісаванне віджэта, яке вяршыць выбор типу візуалізацыі, ўскладненне компонента Widget:

const Widget = ({
  widgetId,
  options,
  filters,
}: {
  widgetId: string;
  options: WidgetOptions;
  filters: WidgetFilters;
}) => {
  const widget = useWidget(widgetId);

  const Component =
    MAP_WIDGET_TYPE_TO_VISUALIZATION[widget.type];

  return (
    <Component
      widgetId={widgetId}
      options={options}
      filters={filters}
    />
  );
};

Ён выбірае спосаб візуалізацыі з MAP_WIDGET_TYPE_TO_VISUALIZATION, а сама візуалізацыя запрашвае свае даны з трох вялічын, ад каторых насправды залежыць:

const LineChart = ({
  widgetId,
  options,
  filters,
}: LineChartProps) => {

  const data = useWidgetData(widgetId, filters, options);
  // ...
};

Жаданых налаштаванняў тут не было. Раздзелэнне абавясцей зробіла граф супаўзалежнасцяў видным у дрэве компанентаў: панель керування должна быць готовая раней, чым будуць вядомы ідэнтыфікаторы віджэтаў; кожны ідэнтыфікатор віджэта адкрыва можлівасці, фільтры і вызначэння самага віджэта; даныя чакаюць толькі своіх рэальных вхідных дадзенняў.

Заставка пра призупіненне хуків

Тое, чы рэальна паралельная адработка гэтых запросоў, залежыць ад таго, як рэалізаваны хукі. Хукі, якія запускаюць запрос і вяртаюць статус загрузкі, дазваляюць кожнаму компоненту адрасавацца адразу, тады ўсе запросы выконваюцца адночасна. Якща ж хукі перываюць свою адработку (напрыклад, запиты у режыме suspense, якія вяртаюць данні безпосередна, што, як выглядае, прымутны з тэксту фрагментаў, калі ў яных чытанае widget.type без пераконтроўкі статусу загрузкі), ситуацыя змінюецца. Два хукі, якія перываюць адработку і вызываемыя адна за другой у там самым компоненте, можу выконвацца па серіі, і дзецінскі компонент, такі як Widget, не адрасаваецца і таму не запускае свой сабэйскі запрос, пакуль яго вышэйшы компонент не перастане перываць адработку. У такой схеме можна знову стварыць ланцоўку адработкі ўсередзіне дрэва компонентаў. Прыбутковая загрузка ў вышэйшым компоненте, адзінутая API для запитоў чы ладчык маршрутаў усунуць гэта. Па спецыяльнам падходу да Suspense адзірніце гэтыя статты.

Паралельные запыткі ў React Suspense.

Галоўныя выводы

  • Кожны await — это заява пра тое, што наступны крок залежыць ад паканальнага рэзультата; пераканайцеся, што кожная заява ўсё ж такі справядліва.
  • Перш чым оптымацаваць, накрэсліце граф залежнасцяў, а потым прыстосавайце код да яго, а не да порядку, у яком вы напісалі лініі.
  • Promise.all рашае проблему незалежных запыткаў, але яго трэба выкорыстоўваць разам з ізоляцыяй ад бягункоў і чысткай эфектаў.
  • Раздзелэнне компанентаў па тым, каму яны прыналежаць, зазвычай дае правільны порядак запыткаў, якщо толькі хукі suspending не серыязуюць яго.