【问题标题】:Code is not executing until after promise completed its job直到 Promise 完成其工作后,代码才会执行
【发布时间】:2019-10-06 10:23:58
【问题描述】:

我的这个问题可能与角度概念无关。但我在角度的背景下遇到它。我点击 Angular 模板中的一个按钮,它会触发 onSubmit 函数。

scanFiles 是一个长时间运行的函数,它返回一个承诺。我希望 console.log("test1") 在长时间运行的功能启动之前打印出来。但这不会发生。它仅在长时间运行功能完成后打印出来。为什么会这样?

    onSubmit(){

        this.scanFiles(this.foldersPath).then((filesPath)=>{
                 //after scan finish 

        })
         .catch((err)=>console.log(err))

        console.log("test1")

    }

2019 年 10 月 7 日进一步更新)

如下所示,我的 scanFiles 函数返回了一个承诺。因此,无论我的 promise 作业做什么,理论上,我认为应该在浏览器开始执行 promise 作业之前打印出“test1”。

scanFiles(foldersPath: any):Promise<string[]> {
        return new Promise(
            (resolveFn, rejectFn)=>{
                try{

                    const scanResult:string[]= foldersPath.reduce(
                        (prevFolderPath:string[], currFolderPath:string)=> {
                            let files:string[] =  this.fileService.getContentInDirectory (currFolderPath, this.filter.bind(this), this.getProcessDirectoryContentFn(), this.subfolderDepthInput)
                            prevFolderPath.push(...files)
                            return prevFolderPath

                        },new Array<string>())
                    console.log(scanResult)
                    resolveFn(scanResult)
                }
                catch(e){
                    console.log(e)
                    rejectFn(e)
                }
            }
        )
``


2019 年 10 月 8 日更新 geContentInDirectory里面有一个readdirSync()函数

getContentInDirectory(dir:string, filterContentFn?:(fullPath:string)=>boolean, processContentFn?:(fullPath:any)=>string, maxSubFolderDepth?:number ): string[]{

        let paths:string[]=[];

        //const dir_NOT_A_DIRECTORY = 
        if(!dir || !fs.lstatSync(dir).isDirectory()) 
            throw new Error("First Parameter must be a directory")

        fs.readdirSync(dir).forEach(entityName=>{
            let fullPath:string = path.join(dir, entityName)
            let isFile = fs.lstatSync(fullPath).isFile()

            if(maxSubFolderDepth==undefined || maxSubFolderDepth >= 0){
                if(isFile){
                    if(filterContentFn) {
                        if(filterContentFn(fullPath)){
                            let content = processContentFn? processContentFn(fullPath): fullPath
                            paths.push(content)
                        }            
                    }
                }
                else {
                    const depth = maxSubFolderDepth==undefined ? undefined: maxSubFolderDepth-1
                    paths.push(...this.getContentInDirectory(fullPath, filterContentFn, processContentFn, depth))
                }
            }
        })
        return paths;

    }
}

2019 年 10 月 8 日更新

我通过这样重写我的代码做了一个实验: 结果是“test0”,“test2”,“test1”依次打印出来。

结论:当 Promise 对象被创建时,它在 Promise 对象中定义的长期运行的作业将被立即触发并执行。 一旦我的 scanFiles 完成了它的工作,“test0”就会被打印出来。

然后在 promise 的 then 函数中注册(注册但尚未执行)回调。然后打印test2"。线程会回到它的事件循环,发现它仍然需要处理回调函数,导致打印"test1"

let p= this.scanFiles(this.foldersPath)
console.log("test0")
p.then((filesPath)=>{
  console.log("test1")
 })
 .catch((err)=>console.log(err))

 console.log("test2")

感谢 Tomalak 用户的解决方案和解释,这导致了我的上述理解。

回答我的问题:我的 Promise 对象内部包含同步任务。难怪我的“test1”(参考我最上面的代码)只有在promise对象中的任务完成后才会打印出来。

【问题讨论】:

  • 在所示的sn-p中,执行顺序是这样的:1)this.scanFiles()被调用,2).then()回调被注册,3).catch()回调被注册,4) console.log() 被调用。如果scanFiles 确实没有阻塞,则在处理scanFiles 结果之前调用console.log()。如果不是这样,那么scanFiles 不会按照您的要求行事。
  • 换句话说,如果没有看到scanFiles的正文,就无法说出发生了什么。
  • 您好,感谢您的回复。我添加了 scanFiles 的正文
  • ...问题就出来了。 getContentInDirectory 使用 fs.readdirSyncfs.lstatSync,这意味着它将阻塞代码。重写它,使其使用fs.readdirfs.lstat 并返回一个promise。然后,您可以一直将该承诺返回到scanFiles 的结果。如果您在节点 10.0.0+ 上,您可以使用 fsPromises 而不是 fs 让您的生活更轻松。
  • “我以为线程会创建一个新的 Promise(),并将其返回给调用者”。不,这不是这样的。 new Promise() 立即 调用它的有效载荷函数。并且在有效载荷函数返回时返回。如果有效负载功能阻塞 10 秒(因为它是同步的,就像您的情况一样),那么 new Promise() 调用将需要 10 秒。 Promise 确实适用于同步代码,但它们并不会神奇地让这个事实消失。您需要一直使用异步代码才能获得任何好处。

标签: angular asynchronous promise


【解决方案1】:

就我对您当前代码的理解而言,这是您的getContentInDirectory 的完全异步、返回承诺的版本。为了测试,我发现 TypeScript 太吵了,把它删了,你觉得合适就加回来。

function getContentInDirectory(dir, filterContentFn, processContentFn, maxSubFolderDepth) {
    return new Promise((resolve, reject) => {
        let result = [], pending = 0;
        function worker(currPath, depth) {
            pending++;
            fs.lstat(currPath, (err, stat) => {
                pending--;
                if (err) return reject(err);
                if (stat.isDirectory()) {
                    if (depth >= 0) {
                        pending++;
                        fs.readdir(currPath, (err, children) => {
                            pending--;
                            if (err) return reject(err);
                            children.forEach(name => worker(path.join(currPath, name), depth - 1));
                        });
                    }
                } else if (!filterContentFn || filterContentFn(currPath)) {
                    result.push(processContentFn ? processContentFn(currPath) : currPath);
                }
                if (!pending) resolve(result);
            });
        }
        worker(dir, maxSubFolderDepth >= 0 ? maxSubFolderDepth : Infinity);
    });
}

有不同的方法来实现这一点,包括使用fs Promises API,它从节点版本 10 开始可用,但至今仍标记为“实验性”。以上没有任何假设,适用于任何节点版本。

现在您可以本着以下精神在scanFiles 中使用它:

function scanFiles(foldersPath) {
    let pendingPaths = foldersPath.map(currFolderPath => {
        return getContentInDirectory(currFolderPath, filterFunc, processFunc, depth);
    });
    return Promise.all(pendingPaths).then(results => {
        return Array.prototype.concat.apply([], results);  // flatten
    });
}

最后在您的事件处理程序中:

onSubmit(){
    this.scanFiles(this.foldersPath).then(filesPath => {
        // after scan finish 
        console.log("this prints last");
    })
    .catch(err => console.log(err));
    console.log("this prints first");
}

这里要注意的是,执行实际工作的函数从一开始就需要是异步的,如果您想成为您的消费函数也是异步的。从 node 的 fs 函数的“同步”版本切换到常规的异步版本是至关重要的一步。

当然,这种变化意味着你程序中getContentInDirectory 的每个使用者都必须更改为异步代码。

【讨论】:

  • 感谢您花时间和耐心提供实现以使我的函数异步。我将使用您提供的代码作为参考,以了解如何将其合并到我的应用程序中。
  • 一旦你有一个返回承诺的函数,你可以开始在任何调用它的函数中使用async/await语义,如果你更喜欢.then()语义。此外,由于您的工作函数仅使用fs.lstat()fs.listdir(),因此您可以专门使用节点的util.promisify() 制作它们的基于promise 的变体。这也可能导致比当前基于回调(“节点样式”)的方法更好的代码。
  • 注意!一旦我做出承诺,将尝试异步/等待。谢谢
  • 糟糕。我不小心删除了您要回答的查询。不过,我会仔细看看,可能是我的程序中的错误。谢谢!
【解决方案2】:

您可以使用多个 Then 链接在一起:

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

这个想法是通过 .then 处理程序链传递结果。

这里的流程是:

初始承诺在 1 秒内解决 (), 然后调用 .then 处理程序 ()。 它返回的值被传递给下一个 .then 处理程序 () ……等等。

链接Promises chaining

或者你可以在执行完之后做一些事情,通过在 finally 中写入:

let isLoading = true;

fetch(myRequest).then(function(response) {
    var contentType = response.headers.get("content-type");
    if(contentType && contentType.includes("application/json")) {
      return response.json();
    }
    throw new TypeError("Oops, we haven't got JSON!");
  })
  .then(function(json) { /* process your JSON further */ })
  .catch(function(error) { console.error(error); /* this line can also throw, e.g. when console = {} */ })
  .finally(function() { isLoading = false; });

【讨论】:

  • 嗨。谢谢回复。但是您的回复似乎没有解决我的问题。我主要担心的是,我希望在承诺开始发挥作用之前先打印出“test1”。但我错了,这就是我问这个问题的原因。
猜你喜欢
  • 1970-01-01
  • 2018-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-25
  • 1970-01-01
  • 1970-01-01
  • 2017-02-24
相关资源
最近更新 更多