卸载组件时,如何取消 React 中的重复承诺(递归函数)?

How do I cancel a repeated promise (Recursive Functions) in React when the component is unmounted?

大家好

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

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

这对我很重要,因为我想告诉用户有多少已选择的文件已成功导入,并且对于在文件导入过程中向 UI 显示每个文件也很有用,这需要我用 Recursive Functions,

问题是 我不知道如何取消使用 Recursive FunctionsPromiseI try with makeCancelable method from react site 它不起作用而且我认为它只是取消了树 Recursive Functions 顶部的 Promise,而不是所有执行的 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 已卸载

自定义承诺 (c-promise) you can do the following (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

尝试使用 useEffect({}, [i])deps 代替 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 的纯粹 :)