JavaScript - 同步等待异步操作(睡眠)

10

我知道这个问题已经被问过很多次,并且很多次都回答说这不是正确的方法,但是再说一遍:)

有没有可能以某种方式调用异步函数(例如定时器/ ajax 调用),基本上是常见的异步任务,并且在等待其结束时同步等待而不会导致 100%CPU 使用率和阻塞浏览器?

简单的答案就够了 - 是或否。如果是“否”,我必须以“异步方式”编写所有依赖于异步操作的代码。否则,它将更好 ;)

想象一下:

updateCSS("someurl.css")

function updateCSS(url) {

   var css = getCachedResource(url);

   css = css.replace(/regexp/gm, function(curUrl) {
     base64 = atob(getCachedResource(curUrl))
     return "data:image;base64," + base64
   })

}

function getCachedResource(url) {
  
  //...
  
  if (cached) {
    
    return cached
    
  } else {
    
    // doajax...    
    // watfor
    
    return loaded
  }
  
}

当然,我可以将其变成异步的,但是代码会非常可怖。此外,我还必须使调用此函数的所有函数都异步化。您能看出简单的解决方案吗?阻塞可能是最简单的。

谢谢


5
答案是。欢迎来到JavaScript编程 :) - Pointy
1
绝对没有任何东西可以将异步转换为同步。如果你停下来想一想,就会明白其中的道理。"代码将变得非常可怕"——拥抱异步性,代码将再次变得美丽。 - Jaromanda X
当足够多的浏览器支持async/await,使其实用化时,答案将会是“是”。但是,有点。 - Pointy
1
@Pointy - 我认为让人们认为异步/等待是暂停执行和等待的行为是很危险的,因为它们实际上并不是。它们只是一些语法糖,告诉解释器隐式地在自动的.then()处理程序中包装后面的代码。其他JS仍然可以像今天一样响应其他事件而运行。它们确实使代码更漂亮,但并没有改变你能做什么或不能做什么。我担心异步/等待将导致越来越多的人不理解异步代码的真正工作原理。 - jfriend00
你的问题得到了回答吗?如果是的话,请点击左侧的绿色复选标记来标记最佳答案。这将向社区表明你的问题已经得到解答,并且遵循正确的程序将为你赚取一些声望点数。 - jfriend00
显示剩余12条评论
7个回答

3

总之,答案是否定的。永远不可能阻止函数或任何其他代码并等待异步操作完成。JavaScript虚拟机没有直接支持它的机制。

特别是在浏览器环境中,无法操纵JavaScript虚拟机事件循环(就像Node中可能的方式以及实际上“deasync”库正在执行的方式一样)。

这是由于JavaScript并发模型和事件处理所造成的,如此处所述:

https://developer.mozilla.org/cs/docs/Web/JavaScript/EventLoop

这与JavaScript是单线程无关。

另外,请注意,所有类似Promise和新功能如await/async的东西只是一种不同的方式,用于编写/管理异步任务的代码。它无法将异步转换为同步,永远都不会有帮助。

所以不要像我一样浪费时间去试图找到一种方法来做到这一点。相反,花时间设计异步代码 ;)


0

是的。

您希望您的代码在“异步函数(例如计时器/ ajax 调用)结束时休眠,而不会使用 100% 的 CPU”。这正是 async/await 所做的。

其他答案和评论都专注于您的短语“同步等待”,在 JS 中意味着阻塞,但我理解您只是想避免以“异步方式”重构代码。您希望直接编写代码,而不必将所有内容结构化为众多回调。

要使用 async/await,您首先必须接受 Promise——异步 JavaScript 的空间/时间。然后,您需要一个像 Chrome 或 Firefox Developer Edition 这样的现代浏览器,或者使用 Babel js 转译您的代码。

接下来,您需要查看 fetch,这是一种返回 Promise 的现代 ajax 方法。

现在您拥有了所需的一切。以下是一个在上述浏览器中运行的示例:

async function fetchy(...args) {
  let response = await fetch(...args);
  if (!response.ok) throw new Error((await response.json()).error_message);
  return response;
}

async function showUser(user) {
  let url = `https://api.stackexchange.com/2.2/users/${user}?site=stackoverflow`;
  let item = (await (await fetchy(url)).json()).items[0];
  console.log(JSON.stringify(item, null, 2));

  let blob = await (await fetchy(item.profile_image)).blob();
  img.src = URL.createObjectURL(blob);
}

input.oninput = async e => await showUser(e.target.value);
showUser(input.value);
<input id="input" type="number" value="918910"></input><br>
<img id="img"></img>
<pre id="pre"></pre>

顺便提一下,fetchy 是我用来处理 fetch 错误的助手。


1
我认为你应该用“不,但是”来开始你的回答,而不是“是的”。await关键字确实让你的代码睡眠/等待,但并不是同步的。 - Bergi
@Bergi,我希望我的回答澄清了“是”的含义。这个问题不知道自己在问什么,在同一个句子中使用“sleep”和“sync wait”(在JS中是矛盾的)。因此,我们必须猜测用户想要什么。我猜测,这个问题的用户渴望看起来同步的代码,但不是“可怕”的代码。 - jib
你真的需要修正你的回答。OP明确要求“同步等待”。这不是async/await所做的。它不能将异步代码转换为同步代码。其他事件处理程序将会运行。如果你认为在等待异步结果时没有其他东西在运行,那么并发问题可能存在。async/await只是语法糖。它不会改变代码的工作方式。它只是使编写异步代码的语法更加愉悦。 - jfriend00
@Fis await 绝对等待,因此得名。它有效地“睡眠”,保持您的上下文不变,并阻止其进度(就像生成器一样),直到异步操作“在没有 100%CPU 使用率和被阻塞的浏览器”的情况下“结束”。这是单线程 JavaScript 中最接近近似线程上下文的东西,除了 Web Workers 外,还有什么更多的要求呢? - jib
@jfriend00 我从未说过它会将代码转换为同步代码。OP明确要求阻塞浏览器,因此显然他们希望其他事情运行。至于“魔法”,我认为你低估了async/await的愿景。 - jib
显示剩余9条评论

0

简单来说,答案是否定的。

不要这么想。应该考虑后端性能,而不是在客户端处理。


在我的情况下无法在服务器上完成。后端是一个愚蠢的“文件”服务器,没有计算能力和脚本/编码可能性。 - Fis
顺便说一下,通过在多个样式表中重用相同的图片来节省带宽... 后端无能为力。此外,我不喜欢那些说:“把一切都移到后端,我们可以在那里进行扩展......”并且编写像猪一样的代码的开发人员。我是一个很传统的人,来自那个每节省一点内存都会有很大帮助、每传输一字节都要花费很多钱的时代。幸运的是,我们现在处于不同的时代,但这并不意味着我们不应该有效地进行优化,只是说“让我们扩展吧”。特别是当客户端足够强大可以在其中执行一些任务时(当然,并非涉及安全问题)。 - Fis

0

不行,即使使用100%的CPU使用率或者被阻塞的浏览器也无法做到。所有的事情都被精确地设计来防止你这样做。

这使得所有异步代码都是有毒的,也就是说,所有调用异步函数(无论是基于Promise还是回调的)的代码本身必须是异步的。

关于糟糕的代码:Promise链和async/await拯救了它们,它们帮助你用更好的语法编写异步代码。 不要害怕,尤其是如果你已经在使用转译器,请使用async/await


1
这并不意味着使用 Promise 或者同步/异步会比同步更好:D 相对于同步,许多其他任务必须以异步方式完成。 - Fis

0

正如其他人所指出的那样,简短的答案是否定的:在大多数使用JavaScript的环境中,它是单线程的。你想要发生的事情是在从getCachedResource()获取缓存资源的另一个回调中处理CSS。

像这样:

function updateCSS(url) {
    getCachedResource(url, function(css) {
        // process the css
    });
}

function getCachedResource(url, done) {
    //...
    if (cached) {
        cachedCSS = ...
        done(cachedCSS);
    } else {
        $.ajax(
        {
            // ...
            success: function(result) {
                // Get the css from the response
                done(css);
            }
            // ...
        });
    }
}

看看jQuery的“deferred”对象或JavaScript Promise,以更完整的方式处理回调


很不幸,这不仅仅是关于单线程的问题,更多的是关于JavaScript虚拟机内部的并发模型和事件循环。正如我之前发布的那样,我必须先阅读这篇文章:https://developer.mozilla.org/cs/docs/Web/JavaScript/EventLoop 然后我才明白什么是异步,永远不能同步完成。而且无论我使用哪种模式或语法糖...回调、Promise、async、await...都无法防止函数返回,一切都是...异步的,并在内部以相同的方式工作。调用异步->事件完成->检查错误->调用完成或错误代码。 - Fis
顺便说一下,如果你看了我的代码,主要问题不在于访问第一个资源,而是在接下来的替换器“循环”中的图像资源,必须以完全不同的异步方式编写,而不是我给你的示例。它将是这样的:获取css->异步完成->从css获取所有url到数组->获取所有数组化资源(缓存/服务器ajax)->异步资源完成->replace(url,atob(resource))->全部完成?是的,donecallback或通过promise嵌套,但实际的代码图形几乎相同。 - Fis
这是一种范式转变:在异步环境中,JavaScript并不通过从函数返回值来工作。它通过将值传递到下一个函数来工作。在您的示例中,您只需要在内部部分重复该模式。调用一个例程来获取信息,并将其传递给一个新函数来处理结果。还要记住,JavaScript函数可以访问封闭函数的局部变量。 - Ray Fischer
async/await/promises 解决了我的问题,在解决第三方库的另一个问题后。我不想相信它会这么好(除了生成的ECMA5代码)。 - Fis

0
有没有可能以某种方式调用异步函数(例如计时器/ ajax调用),基本上是常见的异步任务,并在等待其结束时同步等待,而不会导致100%CPU使用率和阻塞浏览器?
不行。您无法在JavaScript中“同步等待”异步结果。由于它的单线程和事件驱动设计,它不会以这种方式工作。
由于大多数关于此问题的讨论/评论都是概念性的,因此我想提供一个关于如何使用适当的异步设计实际解决特定问题的答案。
JavaScript不提供在某些耗时活动完成时休眠的能力。因此,您无法将您的updateCSS()函数设计为具有纯同步接口。相反,您需要在JavaScript中使用异步工具来设计一个界面,该界面适用于资源被缓存和必须通过网络获取资源的情况。
通常的设计方法是设计一个异步接口,如果资源恰好被缓存,则仍通过异步接口提供它。然后,您有一个调用者可以使用的接口,该接口始终适用于资源是否被缓存。它要求调用者使用异步接口。
实际上,你可以像这样设计 getCachedResource()
function getCachedResource(url) {
  //...
  if (cached) {
    // returns a promise who's resolved value is the resource
    return Promise.resolve(cached);
  } else {
    // returns a promise who's resolved value is the resource
    return doAjax(...);
  }
}

getCachedResource() 方法总是返回一个 Promise,无论值是否已被缓存。因此,调用者始终会像这样使用 promise 接口:

getCachedResource(...).then(function(result) {
    // use the result here
});

如果结果恰好被缓存,.then()处理程序将立即被调用(在下一个时刻)。如果结果是通过网络检索的,则一旦结果可用,.then()处理程序将被调用。调用者不需要担心资源从哪里或以何种方式检索-他们有一个接口来获取它。
updateCSS()函数中,您将不得不为使用getCachedResource()中的异步接口进行设计。因为您当前的设计在css.replace()中使用同步回调,所以您必须稍微重新考虑一下将异步检索适合其中。以下是如何执行此操作的一种方法。
function updateCSS(url) {
   return getCachedResource(url).then(function(css) {
       // preflight the replacements we need to do so we know what to fetch
       var urls = [];
       css.replace(/regexp/gm, function(curUrl) {
           urls.push(curUrl);
       });
       // now load all the resources
       return Promise.all(urls.map(getCachedResource)).then(function(resources) {
           // now have all the resources, in proper order
           var index = 0;
           css = css.replace(/regexp/gm, function(curUrl) {
               return resources[index++];
           });
           // final resolved value is the fully loaded css
           return css;
       })
   });
}

这里的方案是运行一个虚拟的.replace(),它不保留结果以生成您需要加载的URL列表。根据您的正则表达式,这也可以使用.match().exec()来完成。然后,一旦您获得了要获取的URL列表,请获取它们。当您拥有所有这些URL时,然后您可以真正运行.replace(),因为您有可用于在同步.replace()回调中使用的资源。
然后可以像这样使用它:
updateCss(someCSSURL).then(function(css) {
    // use the CSS here
}).catch(function(err) {
    // process error here
});

非常感谢你所做的大量工作!希望这将成为其他人的一个很好的示例。我会以稍微不同的方式来完成,因为我不能(也不想)使用 promises :) - Fis
@Fis - 当然不是,但现代事物的发明是有原因的,因为它们能够解决问题并使生活更轻松。它们通常是我们做事情的“进步”。我不知道为什么仅仅使用 Promises 就必须牵扯出 gulp 和 webpack。是的,如果您打算支持旧版浏览器,您需要一个 polyfill(我建议 bluebird),但这只需要从 CDN 添加一个脚本标签,而不是您所说的所有内容。我可以在没有 Promises 的情况下编写上面的代码,但这将需要 5 倍以上的时间和更多的代码,并且我将重写已经由 Promises 解决的逻辑。 - jfriend00
我需要把所有东西都放在一个可靠的地方(基本上是审查第三方库的源代码,然后使用它们),所以CDN不是一个好主意。但是我已经解决了第三方库(即polyfills)的问题,所以我会使用async/await。 - Fis
是的,没错。所以目前我正在将TypeScript 2.1转换为ES5(没有使用任何其他工具链中的工具 - 如babel),并在旧版浏览器中使用es6-promise polyfill。在IE10+和其他标准浏览器上运行良好。我之所以问这个问题,只是为了确保没有更好的异步处理方式,因为即使使用promises/async/await,与同步相比,代码仍然很复杂。 - Fis
我本来也想避免使用第三方库(比如es6-promise polyfill)因为很多原因,但最终我改变了主意,我将使用/允许使用它们。一旦我将更新的代码推送到github上,我会通知你,这样你就可以检查它了。但首先我需要完成调试,因为在从TS2.0 +回调重写到TS2.1 +承诺+许多其他更新后,它已经崩溃了 :) 但它是Visual Studio解决方案,所以可能对你不感兴趣 :) - Fis
显示剩余5条评论

0

我喜欢其他所有答案,也理解为什么你不能将异步任务变成同步任务。我来到这里是想弄清楚如何使用await等待FileReader对象完成读取,以便我可以使用同一个FileReader对象读取多个文件。

我遇到的问题及其解决方法:FileReader对象不返回Promise,因此您无法使用await

我编写了一个递归函数,在循环中运行第一次读取,并检查.loadend事件何时发生,然后继续下一个文件。对于那些批评者,是的,这可能是不好的做法,但它有效。

回到您的问题,ajax请求是异步的,就像我的例子一样,它不返回Promise,因此您无法使用await。经过一番搜索,我找到了一个stackoverflow帖子,可能可以实际解决我们两个人的问题:

如何等待ajax请求?

你可以编写一个函数,接收URL并返回Ajax调用。这可以使用await进行等待,因此您可以使用.then(doSomethingAfter())以及所有Promises所能做的酷炫操作。我将在尝试后告诉您FileReader如何工作。

~~~~编辑~~~~

这是我的代码:

function getFileResolve(reader, files, currentNum) {
    try {
        var file = files[currentNum];
        return reader.readAsText(file)
    } catch(err) {
        console.log(err)
    }
};

function main() {
    $('#imported-files').on('change', function() {
        var files = this.files,
            reader = new FileReader(),
            currentFileNum = 0;

        reader.onload = function(e) {
            console.log(e.target.result);
            currentFileNum++;

            getFileResolve(reader, files, currentFileNum);
        }
        
        getFileResolve(reader, files, currentFileNum);
    })
};

$(document).ready(main);

ajax调用可以被返回并且可以被视为一个Promise。FileReader对象有一个onloaded事件,如前所述,因此它可以为您调用下一个文件,并且它本身是递归的。希望这能帮助到任何人。#promises_are_hard


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