首先,这是一个非常具体的案例,故意以错误的方式将异步调用改造成一个非常同步的代码库,该代码库有数千行长,而且时间目前无法进行更改以“做”对的。”它伤害了我的每一根纤维,但现实和理想往往无法融合。我知道这很糟糕。
好的,那不碍事了,我该怎么做才能做到:
function doSomething() {
var data;
function callBack(d) {
data = d;
}
myAsynchronousCall(param1, callBack);
// block here and return data when the callback is finished
return data;
}
示例(或缺少示例)都使用库和/或编译器,这两者对于此解决方案都不可行。我需要一个具体的例子来说明如何使它阻塞(例如,在调用回调之前不要离开 doSomething 函数)而不冻结 UI。如果这样的事情在 JS 中是可能的。
“不要告诉我我应该如何以“正确的方式”或其他方式去做”
好的。但你真的应该以正确的方式去做......或者其他什么
“我需要一个具体的例子来说明如何让它阻塞......而不冻结 UI。如果这样的事情在 JS 中是可能的。”
不,不可能在不阻止 UI 的情况下阻止正在运行的 JavaScript。
由于缺乏信息,很难提供解决方案,但一种选择可能是让调用函数进行一些轮询以检查全局变量,然后将回调设置为全局变量 data
。
function doSomething() {
// callback sets the received data to a global var
function callBack(d) {
window.data = d;
}
// start the async
myAsynchronousCall(param1, callBack);
}
// start the function
doSomething();
// make sure the global is clear
window.data = null
// start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
if (window.data) {
clearInterval(intvl);
console.log(data);
}
}, 100);
所有这些都假设您可以修改 doSomething()
。我不知道这是否在卡片中。
如果可以修改,那么我不知道您为什么不将回调传递给 doSomething()
以从另一个回调中调用,但我最好在遇到麻烦之前停下来。 ;)
哦,什么鬼。你举了一个例子,表明它可以正确完成,所以我将展示这个解决方案......
function doSomething( func ) {
function callBack(d) {
func( d );
}
myAsynchronousCall(param1, callBack);
}
doSomething(function(data) {
console.log(data);
});
因为您的示例包含传递给异步调用的回调,所以正确的方法是将函数传递给 doSomething()
以从回调中调用。
当然,如果这是回调唯一要做的事情,您只需直接传递 func
...
myAsynchronousCall(param1, func);
Async functions 是一项功能 in ES2017,它通过使用 promises(一种特定形式的异步代码)和 await
关键字使异步代码看起来同步。还要注意代码示例下方的关键字 async
位于 function
关键字前面,它表示 async/await 函数。如果没有在以 async
关键字为前缀的函数中,await
关键字将不起作用。由于目前没有例外,这意味着没有顶级等待将起作用(顶级等待意味着任何函数之外的等待)。虽然有一个proposal for top-level await
。
ES2017 于 2017 年 6 月 27 日被批准(即最终确定)为 JavaScript 标准。异步等待可能已经在您的浏览器中运行,但如果不是,您仍然可以使用 babel 或 traceur 之类的 javascript 转译器使用该功能。 Chrome 55 完全支持异步功能。因此,如果您有更新的浏览器,您可以尝试下面的代码。
有关浏览器兼容性,请参阅 kangax's es2017 compatibility table。
这是一个名为 doAsync
的异步等待函数示例,它需要三个一秒的暂停,并打印每次暂停后与开始时间的时间差:
函数 timeoutPromise (time) { return new Promise(function (resolve) { setTimeout(function () { resolve(Date.now()); }, time) }) } function doSomethingAsync () { return timeoutPromise(1000); } 异步函数 doAsync () { var start = Date.now(), time;控制台.log(0);时间 = 等待 doSomethingAsync(); console.log(时间 - 开始);时间 = 等待 doSomethingAsync(); console.log(时间 - 开始);时间 = 等待 doSomethingAsync(); console.log(时间 - 开始); } doAsync();
当 await 关键字放在 promise 值之前(在这种情况下,promise 值是函数 doSomethingAsync 返回的值),await 关键字将暂停函数调用的执行,但它不会暂停任何其他函数,它将继续执行其他代码,直到 promise 解决。在 promise 解决之后,它会解开 promise 的值,你可以认为 await 和 promise 表达式现在被解开的值替换。
因此,由于 await 只是暂停等待,然后在执行该行的其余部分之前解包一个值,您可以在 for 循环和内部函数调用中使用它,如下例所示,它收集数组中等待的时间差并打印出数组。
函数 timeoutPromise (time) { return new Promise(function (resolve) { setTimeout(function () { resolve(Date.now()); }, time) }) } function doSomethingAsync () { return timeoutPromise(1000); } // 这会依次调用每个 promise 返回函数 async function doAsync () { var response = []; var start = Date.now(); // 每个索引都是一个 promise 返回函数 var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync]; for(var i = 0; i < promiseFuncs.length; ++i) { var promiseFunc = promiseFuncs[i]; response.push(await promiseFunc() - 开始);控制台日志(响应); } // 对响应做一些事情,响应是来自已解决的承诺的值数组。返回响应 } doAsync().then(function (response) { console.log(response) })
async 函数本身返回一个 Promise,因此您可以像我在上面那样或在另一个 async await 函数中将其用作带有链接的 Promise。
如果您想同时发送请求,上述函数将在发送另一个请求之前等待每个响应,您可以使用 Promise.all。
// 没有变化函数 timeoutPromise (time) { return new Promise(function (resolve) { setTimeout(function () { resolve(Date.now()); }, time) }) } // 没有变化函数 doSomethingAsync () {返回超时承诺(1000); } // 这个函数几乎同时调用异步承诺返回函数 async function doAsync () { var start = Date.now(); // 我们现在使用 Promise all 来等待所有的 Promise 解决 var response = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);返回responses.map(x=>x-start); } // 没有变化 doAsync().then(function (response) { console.log(response) })
如果 promise 可能拒绝,您可以将其包装在 try catch 中或跳过 try catch 并让错误传播到 async/await 函数的 catch 调用。你应该小心不要留下未处理的承诺错误,尤其是在 Node.js 中。下面是一些展示错误如何工作的示例。
function timeoutReject (time) { return new Promise(function (resolve, reject) { setTimeout(function () { reject(new Error("OOPS 你在 TIMESTAMP 遇到错误:" + Date.now())); }, time) }) } function doErrorAsync () { return timeoutReject(1000); } var log = (...args)=>console.log(...args); var logErr = (...args)=>console.error(...args); async function unpropogatedError () { // promise 没有等待或返回,因此它不会传播错误 doErrorAsync(); return "finished unpropogatedError 成功"; } unpropogatedError().then(log).catch(logErr) 异步函数handledError () { var start = Date.now();尝试 { console.log((await doErrorAsync()) - 开始); console.log("过去的错误"); } catch (e) { console.log("在 catch 我们处理了错误"); } return "完成handledError成功"; }handledError().then(log).catch(logErr) // 错误如何传播到链式 catch 方法的示例 async function propogatedError () { var start = Date.now(); var time = await doErrorAsync() - 开始; console.log(时间 - 开始);返回“成功完成propogatedError”; } // 这是打印 propogatedError 错误的内容。 propogatedError().then(log).catch(logErr)
如果您前往 here,您可以看到即将推出的 ECMAScript 版本的已完成提案。
仅可用于 ES2015 (ES6) 的替代方法是使用包装生成器函数的特殊函数。生成器函数有一个 yield 关键字,可以用来复制 await 关键字和周围的函数。 yield 关键字和生成器函数更通用,可以做更多的事情,而不是 async await 函数所做的事情。如果您想要一个可用于复制异步等待的生成器函数包装器,我会查看 co.js。顺便说一句,co 的函数很像异步等待函数返回一个承诺。老实说,虽然此时浏览器兼容性对于生成器函数和异步函数大致相同,所以如果你只想要异步等待功能,你应该使用不带 co.js 的异步函数。(我建议只使用 async/await 它是在支持上述删除线的大多数环境中得到相当广泛的支持。)
除了 IE 之外,所有当前主流浏览器(Chrome、Safari 和 Edge)中的异步功能(截至 2017 年)现在的浏览器支持实际上都非常好。
async/await
函数与 for await 语句一起使用,例如 for await (const item of asyncIterable) { }
当 item
和 asyncIterable
是变量,其余的是关键字。相关链接:Kris Kowal's GTOR 和 asyncIterable proposal repo
看看 JQuery 承诺:
http://api.jquery.com/promise/
http://api.jquery.com/jQuery.when/
http://api.jquery.com/deferred.promise/
重构代码:
var dfd = new jQuery.Deferred(); function callBack(data) { dfd.notify(data); } // do the async call. myAsynchronousCall(param1, callBack); function doSomething(data) { // do stuff with data... } $.when(dfd).then(doSomething);
dfd.notify(data)
的行更新为 dfd.resolve(data)
您可以强制 NodeJS 中的异步 JavaScript 与 sync-rpc 同步。
不过,它肯定会冻结你的 UI,所以当谈到是否有可能走你需要走的捷径时,我仍然是反对者。不可能在 JavaScript 中暂停 One And Only 线程,即使 NodeJS 有时允许您阻止它。在您的承诺解决之前,任何回调、事件、任何异步操作都无法处理。因此,除非您的读者遇到像 OP 这样的不可避免的情况(或者,在我的情况下,正在编写一个没有回调、事件等的美化 shell 脚本),否则不要这样做!
但您可以这样做:
./calling-file.js
var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');
var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"
./my-asynchronous-call.js
function init(initData) {
return function(param1) {
// Return a promise here and the resulting rpc client will be synchronous
return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
};
}
module.exports = init;
限制:
这些都是通过滥用 require('child_process').spawnSync
实现 sync-rpc
的结果:
这在浏览器中不起作用。函数的参数必须是可序列化的。您的参数将传入和传出 JSON.stringify,因此函数和诸如原型链之类的不可枚举属性将丢失。
http://taskjs.org/ 有一个很好的解决方法
它使用对 javascript 来说是新的生成器。所以目前大多数浏览器都没有实现它。我在 Firefox 中对其进行了测试,对我来说这是包装异步函数的好方法。
这是来自项目 GitHub 的示例代码
var { Deferred } = task;
spawn(function() {
out.innerHTML = "reading...\n";
try {
var d = yield read("read.html");
alert(d.responseText.length);
} catch (e) {
e.stack.split(/\n/).forEach(function(line) { console.log(line) });
console.log("");
out.innerHTML = "error: " + e;
}
});
function read(url, method) {
method = method || "GET";
var xhr = new XMLHttpRequest();
var deferred = new Deferred();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 400) {
var e = new Error(xhr.statusText);
e.status = xhr.status;
deferred.reject(e);
} else {
deferred.resolve({
responseText: xhr.responseText
});
}
}
};
xhr.open(method, url, true);
xhr.send();
return deferred.promise;
}
你想要的现在实际上是可能的。如果你可以在 service worker 中运行异步代码,在 web worker 中运行同步代码,那么你可以让 web worker 向 service worker 发送同步 XHR,当 service worker 做异步事情时,web worker 的线程将等待。这不是一个好方法,但它可以工作。
在 Node.js 中,可以编写实际调用异步操作的同步代码。 node-fibers 允许这样做。它是作为 npm 模块提供的第 3 方本机扩展。它实现了纤程/协程,因此当特定纤程被阻塞等待异步操作时,整个程序事件循环不会阻塞 - 另一个纤程(如果存在)继续其工作。
使用纤维,您的代码将如下所示:
var Fiber = require('fibers');
function doSomething() {
var fiber = Fiber.current;
function callBack(data) {
fiber.run(data);
}
myAsynchronousCall(param1, callBack);
// execution blocks here
var data = Fiber.yield();
return data;
}
// The whole program must be wrapped with Fiber
Fiber(function main() {
var data = doSomething();
console.log(data);
}).run();
请注意,您应该避免使用它并改用 async/await
。请参阅下面的项目自述文件 https://github.com/laverdet/node-fibers 中的注释:
过时的注意——这个项目的作者建议你尽可能避免使用它。该模块的原始版本在 2011 年初针对 nodejs v0.1.x,当时服务器上的 JavaScript 看起来有很大不同。从那时起,async/await、Promises 和 Generators 被标准化,整个生态系统也朝着这个方向发展。我会尽可能地继续支持更新版本的 nodejs,但是 v8 和 nodejs 是非常复杂和动态的平台。不可避免地有一天,这个库会突然停止工作,没有人能对此做任何事情。我要感谢所有使用光纤的用户,您多年来的支持对我来说意义重大。
人们可能不会考虑的一件事:如果您控制异步函数(其他代码段依赖),并且它所采用的代码路径不一定是异步的,您可以通过创建使其同步(而不破坏其他代码段)一个可选参数。
目前:
async function myFunc(args_etcetc) {
// you wrote this
return 'stuff';
}
(async function main() {
var result = await myFunc('argsetcetc');
console.log('async result:' result);
})()
考虑:
function myFunc(args_etcetc, opts={}) {
/*
param opts :: {sync:Boolean} -- whether to return a Promise or not
*/
var {sync=false} = opts;
if (sync===true)
return 'stuff';
else
return new Promise((RETURN,REJECT)=> {
RETURN('stuff');
});
}
// async code still works just like before:
(async function main() {
var result = await myFunc('argsetcetc');
console.log('async result:', result);
})();
// prints: 'stuff'
// new sync code works, if you specify sync mode:
(function main() {
var result = myFunc('argsetcetc', {sync:true});
console.log('sync result:', result);
})();
// prints: 'stuff'
当然,如果异步函数依赖于固有的异步操作(网络请求等),这将不起作用,在这种情况下,努力是徒劳的(没有有效地等待空闲旋转)。
同样,根据传入的选项返回值或 Promise 也相当难看。
(“如果它不使用异步构造,为什么我会编写一个异步函数?”有人可能会问?也许函数的某些模式/参数需要异步性而其他不需要,并且由于代码重复,您需要一个整体块而不是在不同的函数中分离模块化的代码块......例如,参数可能是 localDatabase
(不需要等待)或 remoteDatabase
(需要等待)。如果你尝试这样做,你可能会出现运行时错误{sync:true}
在远程数据库上。也许这种情况表明存在另一个问题,但你去吧。)
使用 Node 16 的工作线程实际上使这成为可能,以下示例主线程正在运行异步代码,而工作线程正在同步等待它。
这不是很有用,但它至少通过同步等待异步代码模糊地完成了原始问题。
const {
Worker, isMainThread, parentPort, receiveMessageOnPort
} = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', async () => {
worker.postMessage(await doAsyncStuff());
});
} else {
console.log(doStuffSync());
}
function doStuffSync(){
parentPort.postMessage({fn: 'doStuff'});
let message;
while (!message) {
message = receiveMessageOnPort(parentPort)
}
return message;
}
function doAsyncStuff(){
return new Promise((resolve) => setTimeout(() => resolve("A test"), 1000));
}
Promise 的这种能力包括以下同步操作的两个关键特性(或者 then() 接受两个回调)。得到结果后,调用 resolve() 并传递最终结果。如果出现错误,请调用reject()。
这个想法是结果通过 .then() 处理程序链传递。
const synchronize = (() => {
let chain = Promise.resolve()
return async (promise) => {
return chain = chain.then(promise)
}
})()
let result;
async_function().then(r => result = r);
while (result === undefined) // Wait result from async_function
require('deasync').sleep(100);
您也可以将其转换为回调。
function thirdPartyFoo(callback) {
callback("Hello World");
}
function foo() {
var fooVariable;
thirdPartyFoo(function(data) {
fooVariable = data;
});
return fooVariable;
}
var temp = foo();
console.log(temp);
thirdPartyFoo
正在执行一些异步操作,那么您将在 temp
always 中得到 null
如果您稍微调整一下要求,您希望实现的想法就可以实现
如果您的运行时支持 ES6 规范,则可以使用以下代码。
关于async functions的更多信息
async function myAsynchronousCall(param1) {
// logic for myAsynchronous call
return d;
}
function doSomething() {
var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
return data;
}
SyntaxError: await is only valid in async functions and async generators
。更不用说 param1 没有定义(甚至没有使用)。
callback
函数传递给myAsynchronousCall
函数,该函数执行异步操作并在完成时调用回调。 Here's a demo.doSomething()
通常是整个程序。套用 OP 的话,期望理论编程来反映现实是徒劳的。