【问题标题】:How do I cancel a repeated promise (Recursive Functions) in React when the component is unmounted?卸载组件时,如何取消 React 中的重复承诺(递归函数)?
【发布时间】:2025-12-16 10:40:02
【问题描述】:

大家好

我正在使用React Native,我想添加一些功能,用户可以将多个文件导入我的应用程序,并且用户可以随时取消导入进度

但是,当用户导入这些文件的时候,我要一个一个地导入,告诉用户哪些文件已经导入成功,哪些文件没有导入,

这对我很重要,因为我想告诉用户有多少已选择的文件已成功导入,并且对于在导入该文件时向 UI 显示每个文件很有用,这需要我使用 @987654324 @,

问题是我不知道如何取消使用Recursive FunctionsI try with makeCancelable method from react sitePromise 它不起作用,我认为它只是取消了Promise在树的顶部Recursive Functions,并非所有已执行的 Promise。另外,如果可能的话,我不想使用任何 deps/packages。 有什么想法吗?

核心工具

使用真机Xiaomi Redmi 1S 4.4 Kitkat

"react": "16.13.1",
"react-native": "0.63.3",

代码示例

importFiles.js

import RNFetchBlob from 'rn-fetch-blob';
import CameraRoll from '@react-native-community/cameraroll';
import _ from 'lodash';

const fs = RNFetchBlob.fs;

/**
 * Import directory destination
 */
const dest = `${fs.dirs.SDCardDir}/VEGA/.src/`;

/**
 * An increment index to tell the function which index to run
 */
let i = 0;

/**
 * Import the files to this App with some encryption
 * @param {object} config
 * @param {string} config.albumId
 * @param {[{
 *  uri: string,
 *  mimeType: string,
 *  albumName: string,
 *  timestamp: number,
 *  isSelected: boolean,
 * }]} config.files
 * @param {'fake' | 'real'=} config.encryptionMode
 */
const importFiles = config => {
  return new Promise(async (resolve, reject) => {
    const {albumId, files, encryptionMode} = config;

    if (_.isEmpty(files) || !_.isArray(files)) {
      reject('invalid files');
      return;
    }

    const file = files[i];

    /**
     * It's mean Done when the file got "undefined"
     */
    if (!file) {
      resolve();
      return;
    }

    const uri = file.uri.replace('file://', '');

    try {
      /**
       * Fake Encryption
       *
       * It's fast but not totally secure
       */
      if (!encryptionMode || encryptionMode === 'fake') {
        const md5 = await fs.hash(uri, 'md5');
        const importedFileUri = `${dest}.${md5}.xml`;

        /**
         * TODO:
         * * Test cancelable
         */
        await fs.readFile(uri, 'base64');
        // await fs.mv(uri, importedFileUri);
        // await CameraRoll.deletePhotos([uri]);

        /**
         * If successfully import this file then continue it to
         * the next index until it's "undefined"
         */
        i++;
      }

      /**
       * Real Encryption
       *
       * It's slow but totally secure
       */
      if (encryptionMode === 'real') {
      }

      await importFiles({files, encryptionMode}).promise;
      resolve();
    } catch (error) {
      reject(error);
    }
  });
};

export default importFiles;

FileImporter.js(我如何使用makeCancelable方法

import React, {useEffect} from 'react';
import {View, Alert} from 'react-native';
import {Contrainer, TopNavigation, Text} from '../components/Helper';
import {connect} from 'react-redux';
import utils from '../utils';

const makeCancelable = promise => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      val => (hasCanceled_ ? reject({isCanceled: true}) : resolve(val)),
      error => (hasCanceled_ ? reject({isCanceled: true}) : reject(error)),
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const FileImporter = props => {
  const {userGalleryFiles} = props;

  useEffect(() => {
    props.navigation.addListener('beforeRemove', e => {
      e.preventDefault();

      Alert.alert(
        'Cancel?',
        'Are you sure want to cancel this?',
        [
          {text: 'No', onPress: () => {}},
          {
            text: 'Yes!',
            onPress: () => props.navigation.dispatch(e.data.action),
          },
        ],
        {cancelable: true},
      );
    });

    (async () => {
      const selectedFiles = userGalleryFiles.filter(
        file => file.isSelected === true,
      );

      try {
        await makeCancelable(utils.importFiles({files: selectedFiles})).promise;
        console.warn('Oh God!!!');
      } catch (error) {
        console.error(error);
      }

      return () => makeCancelable().cancel();
    })();
  }, []);

  return (
    <Contrainer>
      <TopNavigation title='Importing files...' disableIconLeft />

      <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
        <Text hint>0 / 20</Text>
      </View>
    </Contrainer>
  );
};

const mapStateToProps = ({userGalleryFiles}) => ({userGalleryFiles});

export default connect(mapStateToProps)(FileImporter);

预期结果

importFiles.js 可以在卸载FileImporter.js 时取消

实际结果

importFiles.js 仍在运行,即使 FileImporter.js 已卸载

【问题讨论】:

标签: javascript reactjs react-native promise cancellation


【解决方案1】:

尝试使用depsuseEffect({}, [i]) 进行反应,而不是使用Recursive Functions

import React, {useEffect, useState} from 'react';
import {View, Alert} from 'react-native';
import {Contrainer, TopNavigation, Text} from '../components/Helper';
import {connect} from 'react-redux';
import utils from '../utils';

const FileImporter = props => {
  const {userGalleryFiles} = props;
  const [currentIndexWantToImport, setCurrentIndexWantToImport] = useState(0)

  useEffect(() => {
    (async () => {
      const selectedFiles = userGalleryFiles.filter(
        file => file.isSelected === true,
      );

      try {
        await utils.importFiles(selectedFiles[currentIndexWantToImport]);
        setCurrentIndexWantToImport(currentIndexWantToImport++);
        console.warn('Oh God!!!');
      } catch (error) {
        console.error(error);
      }
    })();
  }, [currentIndexWantToImport]);

  return (
    <Contrainer>
      <TopNavigation title='Importing files...' disableIconLeft />

      <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
        <Text hint>0 / 20</Text>
      </View>
    </Contrainer>
  );
};

const mapStateToProps = ({userGalleryFiles}) => ({userGalleryFiles});

export default connect(mapStateToProps)(FileImporter);

现在你拥有来自 React 的 Recursive Functions 的纯粹 :)

【讨论】:

  • 哇,这对我来说很有意义,我会试试看:D
【解决方案2】:

使用自定义承诺 (c-promise),您可以执行以下操作 (See the live demo):

import { CPromise, CanceledError } from "c-promise2";

const delay = (ms, v) => new Promise((resolve) => setTimeout(resolve, ms, v));

const importFile = async (file) => {
  return delay(1000, file); // simulate reading task
};

function importFiles(files) {
  return CPromise.from(function* () {
    for (let i = 0; i < files.length; i++) {
      try {
        yield importFile(files[i]);
      } catch (err) {// optionally
        CanceledError.rethrow(err);
        console.log(`internal error`, err);
        // handle file reading errors here if you need
        // for example if you want to skip the unreadable file
        // otherwise don't use try-catch block here
      }
    }
  }).innerWeight(files.length);
}

const promise = importFiles([
  "file1.txt",
  "file2.txt",
  "file3.txt",
  "file4.txt"
])
  .progress((value) => {
    console.log(`Progress [${(value * 100).toFixed(1)}%]`);
    // update your progress bar value
  })
  .then(
    (files) => console.log(`Files: `, files),
    (err) => console.warn(`Fail: ${err}`)
  );

setTimeout(() => promise.cancel(), 3500); // cancel the import sequence

【讨论】:

  • 感谢您的回答,但我不想使用任何部门/包