同步调用一个异步Javascript函数

首先,这是一个非常具体的情况,故意以错误的方式将异步调用改造到非常同步的代码库中,该代码库有数千行之长,目前时间无法提供进行更改以“正确执行”的能力。它伤害了我身体的每一根纤维,但现实和理想往往不相符。我知道这很糟糕。

好了,我要怎么做才能:

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中是可能的。

396993 次浏览

"不要告诉我我应该怎么做"正确的方式"之类的"

好的。但是你应该用正确的方式去做…或者任何< /一口>

“我需要一个具体的例子来说明如何让它成为块……不冻结用户界面。如果这样的事情在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);

看看JQuery的承诺:

http://api.jquery.com/promise/ < a href = " http://api.jquery.com/promise/ " > < / >

http://api.jquery.com/jQuery.when/ < a href = " http://api.jquery.com/jQuery.when/ " > < / >

http://api.jquery.com/deferred.promise/ < a href = " 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);




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;
}

异步函数,一个特性在ES2017,通过使用承诺(一种特殊形式的异步代码)和await关键字使异步代码看起来同步。还要注意在下面的代码示例中,关键字async位于表示async/await函数的function关键字前面。await关键字如果不在带有async关键字前缀的函数中就不能工作。由于目前没有例外,这意味着没有顶级等待将工作(顶级等待意味着任何函数之外的等待)。虽然有一个顶层await的提议

ES2017于2017年6月27日被批准(即最终确定)为JavaScript标准。Async await可能已经在你的浏览器中工作,但如果不是,你仍然可以使用javascript转译器,如巴别塔traceur使用该功能。Chrome 55完全支持异步功能。因此,如果您有一个较新的浏览器,您可以尝试下面的代码。

浏览器兼容性参见Kangax的es2017兼容性表

下面是一个名为doAsync的示例async await函数,它接受三次1秒暂停,并在每次暂停后打印与开始时间的时间差:

function timeoutPromise (time) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(Date.now());
}, time)
})
}


function doSomethingAsync () {
return timeoutPromise(1000);
}


async function doAsync () {
var start = Date.now(), time;
console.log(0);
time = await doSomethingAsync();
console.log(time - start);
time = await doSomethingAsync();
console.log(time - start);
time = await doSomethingAsync();
console.log(time - start);
}


doAsync();

当await关键字被放置在promise值之前(在这种情况下,promise值是由函数doSomethingAsync返回的值),await关键字将暂停函数调用的执行,但它不会暂停任何其他函数,它将继续执行其他代码,直到promise解决。在promise解决后,它将打开promise的值,您可以认为await和promise表达式现在已被已打开的值所取代。

因此,由于await只是暂停等待,然后在执行行剩余部分之前展开一个值,您可以在for循环和内部函数调用中使用它,就像下面的示例一样,收集在数组中等待的时间差并打印出数组。

function timeoutPromise (time) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(Date.now());
}, time)
})
}


function doSomethingAsync () {
return timeoutPromise(1000);
}


// this calls each promise returning function one after the other
async function doAsync () {
var response = [];
var start = Date.now();
// each index is a promise returning function
var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
for(var i = 0; i < promiseFuncs.length; ++i) {
var promiseFunc = promiseFuncs[i];
response.push(await promiseFunc() - start);
console.log(response);
}
// do something with response which is an array of values that were from resolved promises.
return response
}


doAsync().then(function (response) {
console.log(response)
})

async函数本身返回一个promise,所以你可以像我上面所做的那样,在另一个async await函数中使用它作为链接的promise。

上面的函数会在发送另一个请求之前等待每个响应,如果你想并发发送请求,你可以使用Promise.all

// no change
function timeoutPromise (time) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(Date.now());
}, time)
})
}


// no change
function doSomethingAsync () {
return timeoutPromise(1000);
}


// this function calls the async promise returning functions all at around the same time
async function doAsync () {
var start = Date.now();
// we are now using promise all to await all promises to settle
var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
return responses.map(x=>x-start);
}


// no change
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 well you got an error at 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 is not awaited or returned so it does not propogate the error
doErrorAsync();
return "finished unpropogatedError successfully";
}


unpropogatedError().then(log).catch(logErr)


async function handledError () {
var start = Date.now();
try {
console.log((await doErrorAsync()) - start);
console.log("past error");
} catch (e) {
console.log("in catch we handled the error");
}
  

return "finished handledError successfully";
}


handledError().then(log).catch(logErr)


// example of how error propogates to chained catch method
async function propogatedError () {
var start = Date.now();
var time = await doErrorAsync() - start;
console.log(time - start);
return "finished propogatedError successfully";
}


// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

如果你去在这里,你可以看到即将到来的ECMAScript版本的完成提案。

另一种只能在ES2015 (ES6)中使用的方法是使用一个特殊的函数来包装生成器函数。生成器函数有一个yield关键字,可以用它来复制await关键字和一个周边函数。yield关键字和generator函数更通用,可以做更多的事情,而不仅仅是async await函数。如果你想要一个生成器函数包装器,可以用来复制异步等待,我会检查co.js。顺便说一下,co的函数很像async await函数返回一个promise。老实说,在这一点上,生成器函数和async函数的浏览器兼容性是一样的,所以如果你只想要async await功能,你应该使用async函数而不是co.js。 (我建议只使用async/await,它在大多数支持上述擦除的环境中都得到了广泛的支持。

目前除了IE之外,所有主流浏览器(Chrome、Safari和Edge)对Async函数的支持实际上都很好(截至2017年)。

如果稍微调整一下需求
,您希望实现的想法就可以实现

如果您的运行时支持ES6规范,则可以使用下面的代码。

关于异步函数的更多信息

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;
}

可以强制NodeJS中的异步JavaScript与sync-rpc同步。

它肯定会冻结你的UI,所以我仍然是一个反对者,当谈到是否有可能采取你需要采取的捷径。在JavaScript中挂起唯一线程是不可能的,即使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;

限制:

这些都是sync-rpc实现的结果,即滥用require('child_process').spawnSync:

  1. 这将在浏览器中不起作用。
  2. 函数必须的实参是可序列化的。你的参数将传入和传出JSON.stringify,所以函数和不可枚举的属性,如原型链,将会丢失。

你也可以把它转换成回调。

function thirdPartyFoo(callback) {
callback("Hello World");
}


function foo() {
var fooVariable;


thirdPartyFoo(function(data) {
fooVariable = data;
});


return fooVariable;
}


var temp = foo();
console.log(temp);

你想要的现在实际上是可能的。如果你可以在service worker中运行异步代码,在web worker中运行同步代码,那么你可以让web worker向service worker发送同步XHR,当service worker执行异步操作时,web worker的线程将等待。这不是一个很好的方法,但它可以工作。

人们可能不会考虑的一件事是:如果你控制了异步函数(其他代码段依赖于它),并且它所采用的代码路径不一定是异步的,你可以通过创建一个可选参数使它成为同步的(而不破坏其他代码段)。

目前:

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.js中,可以编写调用异步操作的同步代码。 node-fibers允许这样做。它是作为npm模块提供的第三方本地扩展。 它实现了光纤/协程,所以当一个特定的光纤被阻塞等待异步操作时,整个程序事件循环不会阻塞——另一个光纤(如果存在)继续它的工作

使用纤维,你的代码看起来像这样:

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的注释:

过时的注释——这个项目的作者建议你尽量避免使用它。该模块的原始版本针对的是nodejs v0.1。在2011年初,当JavaScript在服务器上看起来有很大的不同。从那时起,异步/等待承诺发电机被标准化了,整个生态系统也朝着这个方向发展。

我会尽可能地继续支持更新版本的nodejs,但是v8和nodejs是非常复杂和动态的平台。不可避免的是,有一天这个图书馆会突然停止工作,没有人能对此做些什么。

我想对所有纤维的用户说声谢谢,你们这些年来的支持对我来说意义重大。

使用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的这种能力包括同步操作的两个关键特性,如下所示(or 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);