[英]Call An Asynchronous Javascript Function Synchronously
首先,這是一個非常具體的案例,故意以錯誤的方式對 retrofit 進行異步調用到非常同步的代碼庫,該代碼庫有數千行長並且時間目前無法進行更改以“做對的。” 它傷害了我的每一根纖維,但現實和理想往往無法融合。 我知道這很糟糕。
好的,那不礙事了,我該怎么做才能做到:
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 function)而不凍結 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);
異步函數是ES2017 中的一項功能,通過使用promise (一種特殊形式的異步代碼)和await
關鍵字使異步代碼看起來同步。 還要注意在下面的代碼示例中, function
關鍵字前面的關鍵字async
表示 async/await 函數。 如果不在以async
關鍵字為async
的函數中,則await
關鍵字將無法工作。 由於目前沒有例外,這意味着沒有頂級等待將起作用(頂級等待意味着在任何函數之外等待)。 盡管有頂級await
的建議。
ES2017 於 2017 年 6 月 27 日被批准(即最終確定)為 JavaScript 的標准。 Async await 可能已經在您的瀏覽器中工作,但如果沒有,您仍然可以使用babel或traceur等 javascript 轉譯器使用該功能。 Chrome 55 完全支持異步功能。 因此,如果您有較新的瀏覽器,則可以嘗試以下代碼。
有關瀏覽器兼容性,請參閱kangax 的 es2017 兼容性表。
這是一個名為doAsync
異步等待函數doAsync
,它需要三個一秒的暫停,並在每次暫停后從開始時間打印時間差:
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 關鍵字放在承諾值之前(在這種情況下,承諾值是函數 doSomethingAsync 返回的值),await 關鍵字將暫停函數調用的執行,但不會暫停任何其他函數,它將繼續執行其他代碼直到承諾解決。 在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) })
異步函數本身返回一個承諾,因此您可以將其用作鏈接的承諾,就像我上面所做的那樣,或者在另一個異步等待函數中使用。
上面的函數會在發送另一個請求之前等待每個響應,如果你想同時發送請求,你可以使用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) })
如果承諾可能拒絕,您可以將其包裝在 try catch 中或跳過 try catch 並讓錯誤傳播到 async/await 函數 catch 調用。 你應該小心不要讓 promise 錯誤得不到處理,尤其是在 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 關鍵字和生成器函數具有更通用的用途,可以做更多的事情,而不是 async await 函數所做的事情。 如果你想要一個可用於復制異步等待的生成器函數包裝器,我會查看co.js 。 順便說一下,co 的函數很像 async await 函數返回一個 promise。 老實說,雖然在這一點上,生成器函數和異步函數的瀏覽器兼容性大致相同,因此如果您只想要異步等待功能,您應該使用沒有 co.js 的異步函數。 (我建議只使用 async/await,它在支持上述刪除線的大多數環境中都得到了廣泛支持。)
現在,除 IE 之外的所有主流瀏覽器(Chrome、Safari 和 Edge)中的 Async 功能(截至 2017 年)的瀏覽器支持實際上都非常好。
看看 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);
您可以強制 NodeJS 中的異步 JavaScript 與sync-rpc 同步。
不過,它肯定會凍結您的用戶界面,所以當談到是否可以采取您需要采取的快捷方式時,我仍然持反對意見。 在 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
:
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 的線程將等待。 這不是一個很好的方法,但它可以工作。
人們可能不會考慮的一件事:如果您控制異步函數(其他代碼段所依賴的),並且它所采用的代碼路徑不一定是異步的,您可以通過創建同步(不破壞其他代碼段)一個可選參數。
目前:
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
(不需要await)或remoteDatabase
(需要)。那么如果您嘗試執行{sync:true}
則可能會運行時錯誤{sync:true}
在遠程數據庫上。也許這種情況表明存在另一個問題,但你去了。)
在 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 是非常復雜和動態的平台。 不可避免地有一天,這個庫會突然停止工作,沒有人能夠對此做任何事情。
我想對光纖的所有用戶說聲謝謝,你們多年來的支持對我來說意義重大。
使用 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)
}
})()
使用Async Await和Promise.resolve / Promise.all😊
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);
如果您稍微調整一下需求,您希望實現的想法就可以實現
如果您的運行時支持 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;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.