Как я могу использовать Мьютекс с реактивным хуком?

#javascript #react-hooks

#javascript #реагирующие крючки

Вопрос:

Я хочу быть уверен, что блок кода не выполняется одновременно. Однако библиотека async-Mutex, похоже, не работает для меня. Вот минимальная репликация:

 /* eslint-disable */
import React, { useState, useEffect } from "react";
var Mutex = require("async-mutex").Mutex;

const Timer = () => {
  const [isActive, setIsActive] = useState(false);
  const mutex = new Mutex();

  useEffect(() => {
    mutex.runExclusive(async function () {
      console.log("starting", isActive);
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log("ending", isActive);
    });
  }, [isActive]);

  return (
    <div className="app">
      <button onClick={() => setIsActive((prev) => !prev)} type="button">
        {isActive ? "Active" : "Inactive"}
      </button>
    </div>
  );
};

export { Timer as default };

 

Код И поле

Если вы дважды быстро нажмете на кнопку, вы увидите, что она будет печатать

 starting 
true
starting 
false
starting 
true
ending 
true
ending 
false
 

Вы можете видеть, что они чередуются.

Как я могу заставить это не выполняться одновременно?

Ответ №1:

ДЕМОНСТРАЦИЯ

 import React, { useState, useEffect, useRef, useMemo} from "react";
var Mutex = require("async-mutex").Mutex;

const Timer = () => {
  const [isActive, setIsActive] = useState(false);
  const mutex = useRef(new Mutex());
  // Or another way, it might be a bit more performant,
  // although the Mutex class has an almost empty constructor
  // const mutex= useMemo(()=> new Mutex(), []);

  useEffect(() => {
    mutex.current.runExclusive(async function () {
      console.log("starting", isActive);
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log("ending", isActive);
    });
  }, [isActive]);

  return (
    <div className="app">
      <button onClick={() => setIsActive((prev) => !prev)} type="button">
        {isActive ? "Active" : "Inactive"}
      </button>
    </div>
  );
};

export { Timer as default };
 

На случай, если вы хотите отменить предыдущий вызов функции и поддерживать очистку асинхронных подпрограмм при размонтировании (демонстрация):

 /* eslint-disable */
import React, { useState, useEffect } from "react";
import CPromise from "c-promise2";
import { useAsyncEffect, useAsyncCallback } from "use-async-effect2";

export default function Timer() {
  const [isActive, setIsActive] = useState(false);

  const callback = useAsyncCallback(
    function* () {
      console.log("starting", isActive);
      yield CPromise.delay(1000);
      console.log("ending", isActive);
    },
    { combine: true }
  );

  useAsyncEffect(
    function* () {
      console.log("mount");
      yield callback();
    },
    [isActive]
  );

  return (
    <div className="app">
      <button onClick={() => setIsActive((prev) => !prev)} type="button">
        {isActive ? "Active" : "Inactive"}
      </button>
    </div>
  );
}
 

Ответ №2:

Проблема, скорее всего, вызвана тем, что мьютекс выделяется при каждом повторном рендеринге, предоставляя вам новый экземпляр.

  1. Вы можете переместить const mutex = new Mutex(); out из своего компонента, предоставив вам одну глобальную версию для всех ваших компонентов таймера, что означает, что таймеры могут блокировать друг друга.
  2. Вы могли бы сделать экземпляр мьютекса стабильным во время повторного рендеринга, обернув его в React.useMemo хук, при этом у каждого компонента таймера был бы свой собственный мьютекс, что означает, что таймеры не могли блокировать друг друга, только себя.

Замените ваше объявление внутри вашего компонента на:

 const mutex = React.useMemo(() => new Mutex(), []);
 

Затем добавьте мьютекс в свой массив зависимостей в useEffect :

 useEffect(() => {
  // Your code
}, [isActive, mutex]);