React.js 建议取消 Promise,但官方 Promise 无法取消。那我应该怎么做?

8
为了防止未挂载的React组件出现幽灵更新,React告诉你在组件(例如用于获取额外数据的promise)卸载时取消任何待定的promise。使用Bluebird promises非常容易实现这一点,因为它们有一个.cancel()方法,使得.then().catch()处理程序永远不会响应。
然而,ES6 Promises不支持取消。此外,ES7的asyncawait只使用原生Promises,不支持任何可替换项(如Bluebird)。这意味着如果你想能够在React中取消Promises(正如他们告诉你要做的那样),你必须使用.then().catch(),并且还必须在原生Promise方法(如fetch())上放置一个中间层以便它可以被取消。
这真的是React所期望的吗?

一种常见的模式是将异步加载移动到您的存储中(类似于Redux),并使React组件内部的代码严格同步。这并不意味着React即将退出舞台,而是React团队已经确定,在视图中混合异步代码会导致更多问题,尽管它具有短期方便和速度。您还可以阅读2015年的文章,了解其他解决此问题的方法,例如isMounted是反模式 - Ross Allen
1
请问您能否在其中包含React文档页面的链接? - x00
我建议从React自带的useReducer开始,以了解应用程序状态的中央存储对象的流程。还有其他关于使用useReducer进行异步获取的问题,这也可以使您的设置更完善。 - Ross Allen
你尝试在内部添加setTimeout来抛出错误或拒绝Promise了吗? - Psartek
如果你只想要可取消的 Promise,这里有一个使用 Promise 封装的 monkey patch 的 jsfiddle:https://jsfiddle.net/x6ah7qog/1/。不过,它有点不太优雅,需要注意。 - user120242
问题中的链接(关于isMounted)实际上有一个很好的makeCancellable包装器示例,可用于通用es6 promise周围。当您无法将异步进程抽象为更集中的内容时,似乎是一个不错的解决方法。 - tmdesigned
2个回答

2
最新的React文档(2023年8月)提出了两种解决这个问题的方法:
  1. 在Effect清理期间创建一个新的“ignore”变量,并将其设置为true。当Promise返回时,根据该变量忽略结果。

  2. 使用AbortController API来取消异步操作。

请查看React文档中“挑战4的解决方案”,位于此页面上。

1
中止信号并不会取消承诺,它只会取消异步操作(例如HTTP获取)。 - Bergi

0

仅供参考。 使用CPromise包,您可以取消您的Promise链,包括嵌套的链。它支持AbortController和生成器作为ECMA异步函数的替代品。目前该项目处于beta阶段。

生成器用法Live Demo

import CPromise from "c-promise2";

const chain = CPromise.resolve()
  .then(function* () {
    const value1 = yield new CPromise((resolve, reject, { onCancel }) => {
      const timer = setTimeout(resolve, 1000, 3);
      onCancel(() => {
        console.log("timer cleared");
        clearTimeout(timer);
      });
    });
    // Run promises in parallel using CPromise.all (shortcut syntax)
    const [value2, value3] = yield [
      CPromise.delay(1000, 4),
      CPromise.delay(1000, 5)
    ];
    return value1 + value2 + value3;
  })
  .then(
    (value) => {
      console.log(`Done: ${value}`); // Done: 12 (without calling cancel)
    },
    (err) => {
      console.log(`Failed: ${err}`); // Failed: CanceledError: canceled
    }
  );

setTimeout(() => chain.cancel(), 100);

输出:

timer cleared 
Failed: CanceledError: canceled 

所有阶段都可以完全取消/中止。 以下是在React中使用它的示例实时演示

export class TestComponent extends React.Component {
  state = {};

  async componentDidMount() {
    console.log("mounted");
    this.controller = new CPromise.AbortController();
    try {
      const json = await this.myAsyncTask(
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
      );
      console.log("json:", json);
      await this.myAsyncTaskWithDelay(1000, 123); // just another async task
      this.setState({ text: JSON.stringify(json) });
    } catch (err) {
      if (CPromise.isCanceledError(err)) {
        console.log("tasks terminated");
      }
    }
  }

  myAsyncTask(url) {
    return CPromise.from(function* () {
      const response = yield cpFetch(url); // cancellable request
      return yield response.json();
    }).listen(this.controller.signal);
  }

  myAsyncTaskWithDelay(ms, value) {
    return new CPromise((resolve, reject, { onCancel }) => {
      const timer = setTimeout(resolve, ms, value);
      onCancel(() => {
        console.log("timeout cleared");
        clearTimeout(timer);
      });
    }).listen(this.controller.signal);
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }
  componentWillUnmount() {
    console.log("unmounted");
    this.controller.abort(); // kill all pending tasks
  }
}

使用 Hooks 和 cancel 方法

import React, { useEffect, useState } from "react";
import CPromise from "c-promise2";
import cpFetch from "cp-fetch";

export function TestComponent(props) {
  const [text, setText] = useState("fetching...");

  useEffect(() => {
    console.log("mount");
    const promise = cpFetch(props.url)
      .then(function* (response) {
        const json = yield response.json();
        setText(`Delay for 2000ms...`);
        yield CPromise.delay(2000);
        setText(`Success: ${JSON.stringify(json)}`);
      })
      .canceled()
      .catch((err) => {
        setText(`Failed: ${err}`);
      });

    return () => {
      console.log("unmount");
      promise.cancel();
    };
  }, [props.url]);

  return <p>{text}</p>;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接