Каков порядок выполнения эффекта EE и его внутренняя логика очистки в ответных хуках? - PullRequest
0 голосов
/ 14 декабря 2018

Согласно документу реагирования, useEffect запустит логику очистки перед повторным запуском useEffect part.

Если ваш эффект возвращает функцию, React запустит ее, когда онавремя на очистку ...

Не существует специального кода для обработки обновлений, потому что useEffect обрабатывает их по умолчанию.Он очищает предыдущие эффекты перед применением следующих эффектов ...

Однако, когда я использую requestAnimationFrame и cancelAnimationFrame внутри useEffect, я обнаружил, что cancelAnimationFrame может не остановить анимацию нормально,Иногда я обнаруживал, что старая анимация все еще существует, в то время как следующий эффект приносит другую анимацию, которая вызывает проблемы с производительностью моего веб-приложения (особенно когда мне нужно визуализировать тяжелые элементы DOM).

Я не знаю, реагирую лиhook выполнит некоторые дополнительные действия, прежде чем выполнит код очистки, из-за чего моя часть cancel-animation не будет работать нормально, useEffect hook выполнит что-то вроде замыкания для блокировки переменной состояния?

Что такое выполнение useEffect?порядок и его внутренняя логика очистки?Что-то не так в коде, который я пишу ниже, из-за чего cancelAnimationFrame не может работать идеально?

Спасибо.

//import React, { useState, useEffect } from "react";

const {useState, useEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState(Math.random());
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Ответы [ 3 ]

0 голосов
/ 14 декабря 2018

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

Согласно документации:

useEffect запускается после реакции, визуализирует ваш компонент и гарантирует, что обратный вызов эффекта не блокирует рисование в браузере.Это отличается от поведения компонентов класса, где componentDidMount и componentDidUpdate запускаются синхронно после рендеринга.

и, следовательно, использование requestAnimationFrame в этих жизненных циклах работает, по-видимому, незначительно, но с небольшим затруднением при useEffect,И поэтому следует использовать useEffect, когда изменения, которые вы должны внести, не блокируют визуальные обновления, такие как вызовы API, которые приводят к изменению DOM после получения ответа.

Еще один крючок, который менее популяренно очень удобно при работе с визуальными обновлениями DOM useLayoutEffect. Согласно документации

Подпись идентична useEffect, но она запускается синхронно после всех мутаций DOM.Используйте это для чтения макета из DOM и синхронного повторного рендеринга.Обновления, запланированные внутри useLayoutEffect, будут сбрасываться синхронно до того, как браузер сможет рисовать.

Итак, если ваш эффект изменяет DOM (через ссылку на узел DOM) и мутация DOM будетизмените внешний вид узла DOM между моментом его рендеринга и его мутацией, а затем вы не хотите использовать useEffect.Вы захотите использовать useLayoutEffect.В противном случае пользователь может увидеть мерцание, когда ваши DOM-мутации вступят в силу, что в точности соответствует requestAnimationFrame

//import React, { useState, useEffect } from "react";

const {useState, useLayoutEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState("");
  const [progress, setProgress] = useState(0);

  useLayoutEffect(() => {
    setStartSeconds(Math.random());

    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useLayoutEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
0 голосов
/ 06 марта 2019

Одна вещь, которая не ясна в ответах выше, - это порядок, в котором запускаются эффекты, когда в миксе несколько компонентов.Мы выполняем работу, которая включает в себя координацию между родителем и его детьми через useContext, поэтому порядок важнее для нас.useLayoutEffect и useEffect работают в этом отношении по-разному.

useEffect запускает очистку и новый эффект перед переходом к следующему компоненту (сначала на глубину) и делает то же самое.

useLayoutEffect запускает очистку каждого компонента (сначала глубина), затем запускает новые эффекты всех компонентов (сначала глубина).

render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent
const Test = (props) => {
  const [s, setS] = useState(1)

  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return (
    <>
      <button onClick={() => setS(s+1)}>update {s}</button>
      <Child name="a" />
      <Child name="b" />
    </>
  )
}

const Child = (props) => {
  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return <></>
}
0 голосов
/ 14 декабря 2018

Поместите эти три строки кода в компонент, и вы увидите их порядок приоритета.

  useEffect(() => {
    console.log('useEffect')
    return () => {
      console.log('useEffect cleanup')
    }
  })

  window.requestAnimationFrame(() => console.log('requestAnimationFrame'))

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
    return () => {
      console.log('useLayoutEffect cleanup')
    }
  })

useLayoutEffect > requestAnimationFrame > useEffect

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

Дальнейшее тестирование показало, что useLayoutEffect всегда вызывается до requestAnimationFrame и что его функция очистки вызывается до следующего выполнения, предотвращаяперекрытия.

Измените useEffect на useLayoutEffect, и это должно решить вашу проблему.

useEffect и useLayoutEffect вызываются в порядке их появления вВаш код для похожих типов, таких как useState звонки.

Вы можете увидеть это, запустив следующие строки:

  useEffect(() => {
    console.log('useEffect-1')
  })
  useEffect(() => {
    console.log('useEffect-2')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-1')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
  })
...