如何调用异步JavaScript函数并阻止原始调用者。

20

我遇到了一个有趣的情况,即使我通常聪明的头脑也无法想出解决方案:)以下是情况描述...

我有一个类,其中包括一个get()方法...调用此方法以获取用户存储的首选项...它会调用一些基础提供程序来实际获取数据...如今,它正在调用一个涉及cookie的提供程序...因此,get()调用providerGet()(例如),providerGet()返回一个值,然后get()将其传递给调用者。显然,在继续工作之前,调用者需要响应。

这里是棘手的部分...我现在正试图实现一个异步的提供程序(在这种情况下使用本地存储)...因此,providerGet()会立即返回,并分派一个调用回调函数的本地存储调用,稍后将调用该函数...但是,由于providerGet()已经返回,而且get()也随之扩展到最初的调用方,显然它还没有返回实际检索到的数据。

因此,问题很简单,是否有一种方法可以阻止providerGet()的返回,直到异步调用返回?请注意,出于我的目的,我不关心这可能产生的性能影响,我只是试图弄清楚如何使其工作。

我认为没有办法,当然我知道我自己也想不出来...所以我想看看其他人能想出什么 :)

编辑:我现在才知道问题的核心,即Web SQL API是异步的事实,可能有一个解决方案......原来还有API的同步版本,这是我没有意识到的...我正在阅读文档,看看如何使用它,但这会很好地解决问题,因为providerGet()被编写成异步的唯一原因是为了允许该提供程序...get()所在的代码是我自己的抽象层,位于各种存储提供程序(cookie、Web SQL、localStorage等)之上,所以最低公共分母必须获胜,这意味着如果其中一个是异步的,它们都必须是异步的...唯一异步的是Web SQL...所以如果有一种方法可以同步进行,我的观点就无效了(尽管我认为这仍然是一个有趣的泛型问题)。

编辑2:啊,好像没有帮助......似乎API的同步版本没有在任何浏览器中实现,即使它被实现了,规定它只能从工作线程中使用,所以这似乎也不能帮助。不过,阅读其他一些内容,听起来有一种递归的方法可以完成这个技巧...我正在组合一些测试代码,如果/当我使它正常工作,我会发布它,似乎是一种非常有趣的方法来通用地解决这种情况。

编辑3:根据我下面的评论,没有办法完全做到我想要的。我正在采取的解决我的直接问题的方法是根本不允许使用Web SQL进行数据存储。这不是理想的解决方案,但由于该规范正在变化,并且不被广泛实现,因此这并不是世界末日...希望在其得到适当支持时将提供同步版本,我可以插入一个新的提供程序,并且很好地工作。一般地说,似乎没有任何方法可以完成这个奇迹......证实了我所预料的情况,但我希望这次我能错。


不,真的没有办法做到这点。真正的解决方案是反转你的 API,这样你自己的 "get()" 就会传递一个回调函数。 - Pointy
另一个可能性是,如果您无法更改API,则可以在加载时将存储在本地存储中的任何内容直接迁移到页面中(或其他方式)。 如果存储的内容不太多,则可以在需要时直接从“providerGet()”实现中访问它。 - Pointy
看看这个项目,它可能正是你需要的... https://github.com/JeffreyZhao/jscex - Dagg Nabbit
使用外部状态和进度检查,有时可以抛出setTimeout(arguments.callee.bind(this,arguments,100);并稍后继续执行,但你的老板不会喜欢那种代码... - dandavis
5个回答

15

创建一个WebWorker线程来执行异步操作。将执行任务所需的信息以及唯一标识符传递给它。关键是在完成后让它将结果发送到Web服务器。

同时,产生WebWorker的函数向同一Web服务器发送ajax请求,使用xmlhttprequest对象的同步标志(是的,它有同步选项)。由于它将阻塞直到HTTP请求完成,因此您可以编写Web服务器脚本来轮询数据库以获取更新或其他操作,直到结果已发送到Web服务器。

虽然有点丑陋,但不会占用CPU资源 :D

基本上如此。

function get(...) {
    spawnWebworker(...);
    var xhr = sendSynchronousXHR(...);
    return xhr.responseTEXT;
}

3
我喜欢厉害的技巧!+1 - Tibos
有没有任何方法可以在不阻塞用户界面的情况下利用这种方法?我正在拼命寻找一种解决方案来应对showModalDialog API的删除 - Brett Postin
@BrettPostin,但现在我们有对话 API,不是吗? - Anthony Hunt
@AnthonyHunt 我不认为Dialog API会像alert/confirm/showModalDialog一样阻塞。 - Brett Postin

12

不,你不能在异步调用完成之前阻塞。就是这么简单。

听起来你可能已经知道了这一点,但如果你想使用异步的ajax调用,那么你必须重构你的代码结构。你不能只有一个.get()方法来进行异步的ajax调用,阻塞直到它完成并返回结果。在这种情况下最常用的设计模式(例如查看谷歌的所有网络javascript API),是让调用者传递一个完成函数。调用.get()将启动异步操作,然后立即返回。当操作完成时,将调用完成函数。调用者必须相应地结构化他们的代码。

当使用异步网络时,无法编写直接、顺序的过程性javascript代码,例如:

var result = abc.get()
document.write(result);

最常见的设计模式是这样的:

abc.get(function(result) {
    document.write(result);
});
如果你的问题存在多个调用层级,那么回调函数可以沿着不同的级别传递,并在需要时被调用。
FYI,新版本的浏览器支持 Promise 的概念,可以与 asyncawait 一起使用来编写类似以下代码的代码:
async function someFunc() {
    let result = await abc.get()
    document.write(result);
}

这仍然是异步的。它仍然是非阻塞的。abc.get()必须返回一个解决为值result的Promise。这段代码必须在声明为async的函数内部,而此函数外部的其他代码将继续运行(这就是使其非阻塞的原因)。但是,在特定函数内部的本地代码看起来更像是阻塞式代码。


是的,你完全正确,我非常清楚这一点 :) 我原本希望有一个聪明的解决方案,但事实上并没有。我想到的递归思路也不行...理论上,如果JavaScript不是单线程的话,它会起作用...但是一旦你进入一个递归循环,并一遍又一遍地调用getProvider()函数,这就是这种思路所要求的,你的异步函数将永远无法有效地完成(事实上,在我的测试中,由于过多的递归,你会导致浏览器崩溃)。所以,是的,我挂了,这是最后的结论 :( - Frank W. Zammetti
@Frank W. Zammetti 你可以让递归循环在检查之间异步休眠,以便让其他函数完成,但你可能不应该这样做。 - Will Chen

3
为什么不让原始调用者传递自己的回调函数给 get() 呢?这个回调函数将包含依赖于响应的代码。 get() 方法会将回调函数转发给 providerGet(),当它调用自己的回调函数时,就会调用它。
抓取结果将传递给原始调用者的回调函数。
function get( arg1, arg2, fn ) {
    // whatever code

    // call providerGet, passing along the callback
    providerGet( fn );
}

function providerGet( fn ) {
    // do async activity

    // in the callback to the async, invoke the callback and pass it the data
    // ...
          fn( received_data );
    // ...
}

get( 'some_arg', 'another_arg', function( data ) {
    alert( data );
});

很遗憾,该调用者是 UI 库的一部分,因此这样的更改不可行... get() 也作为 UI 小部件的渲染的一部分被调用(它检索用户可能已更改的网格列的宽度)。 - Frank W. Zammetti
@FrankW.Zammetti:您是根据设计还是要求来制作新的异步代码?换句话说,能否同步执行它?我猜不太明白为什么您希望它是异步的,但同时又想要它阻塞。 - RightSaidFred
是的,它是同步设计的...问题在于底层提供程序(其中providerGet()是其中之一)可以是同步或异步的... get()方法是在其上面的抽象层中的一部分...并且复杂性在于调用get()的调用者是一个UI库,在构建UI时调用它(例如,在构建网格小部件时,它需要检索列的宽度,这是可更改和可存储的每个用户)...然而,看起来有一个不错的递归解决方案,我正在努力编写一些示例代码,并将在其工作时发布它。 - Frank W. Zammetti
@FrankW.Zammetti:你拿到之后给我发个通知,我想看看你的意思。如果我理解正确,providerGet()可以是同步的,但它的调用者却将它变成了异步(或者至少没有表达出偏好,默认为异步)。是这样吗? - RightSaidFred

1

这很丑陋,但我认为问题有点暗示需要一个丑陋的解决方案...

  1. 在您的get函数中,将查询序列化为字符串。
  2. 打开一个iframe,将(A)此序列化查询和(B)一个随机数传递给此iframe
    • 您的iframe具有一些JavaScript代码,可以从自己的查询字符串中读取SQL查询和数字
    • 您的iframe 异步地开始运行查询。
    • 当您的iframe查询异步完成时,它会将其与随机数字一起发送到您的服务器,例如发送到/write.php?rand=###&reslt="blahblahblah"
    • Write.php将此信息保存在某个位置
  3. 回到您的主脚本,在创建和加载iframe之后,创建一个同步的AJAX请求到您的服务器,例如发送到/read.php?rand=####
  4. /read.php阻塞,直到可用写入信息,然后将其返回到主页

为避免将数据发送到网络中,您可以让iframe将结果编码为浏览器缓存的通过canvas生成的图像(类似于Zombie cookie所使用的方法)。然后,您的阻止脚本将尝试不断加载此图像(每个请求都会带有一些小的网络延迟),直到可用缓存版本,可以通过设置的标志来识别已完成。


哎呀,我错过了Chris的答案,其实基本上是一样的... :-/ - Fabio Beltramini

1
当您的异步方法开始时,我会打开某种模态对话框(用户无法关闭),告诉他们请求正在处理中。当请求完成时,在回调函数中关闭模态框。
一种可能的方法是使用 jqModal,但这需要您将jQuery加载到项目中。我不确定这对您是否可行。

@user886931 - 仅仅为了显示一个模态对话框而加载jQuery,即使是我也有点过分了 :) - Adam Rackis
有趣的建议...在用户可以意识到正在发生什么的情况下是可行的,但在这种情况下不起作用:get()方法的调用者实际上是UI工具包代码,它获取的是用户在UI呈现时可能已修改的网格中列的宽度。 - Frank W. Zammetti
这个问题并没有提到UI或用户方面的内容,它更多关注于代码。模态框其实就是一个覆盖在所有其他元素之上的div,你不需要jQuery来实现,只需要使用纯CSS即可。 - aziz punjani
@Frank - 那么你需要调整你的API,像上面RightSaidfred所说的那样,提供自己的回调函数。 - Adam Rackis
@Interstellar_Coder - 我认为他想要阻止程序,直到请求完成,而不让用户做任何事情。你完全正确,你不需要jQuery来创建一个模态框。但是如果他已经在使用jQuery,我认为jqModal是一种简单的实现方式。只是尽可能地提供信息给op,让他自己决定 :) - Adam Rackis
你说得对,我在这个上下文中可能不应该使用“阻塞”这个术语,我的错...从技术上讲,它是准确的,因为我想要阻塞代码直到异步调用完成(这实际上使其不是异步的,但我离题了!)...但我当然可以理解为什么会导致模态对话框。 - Frank W. Zammetti

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