簡體   English   中英

異步調用 Javascript Function 同步

[英]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 可能已經在您的瀏覽器中工作,但如果沒有,您仍然可以使用babeltraceur等 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

  1. 這在瀏覽器中不起作用。
  2. 函數的參數必須是可序列化的。 您的參數將傳入和傳出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/awaitPromisesGenerators被標准化,整個生態系統都朝着這個方向發展。

我將盡可能繼續支持較新版本的 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM