同步调用异步Javascript函数

时间:2012-02-03 00:03:01

标签: javascript asynchronous

首先,这是一个非常具体的例子,它将错误的方式用于将异步调用改造成一个非常同步的代码库,这个代码库有数千行,并且时间目前无法提供进行更改的能力“做得对。”它伤害了我生命中的每一根纤维,但现实和理想往往不会啮合。我知道这很糟糕。

好的,我不知道如何做到这一点:

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中可以做到这一点。

13 个答案:

答案 0 :(得分:117)

  

“不要告诉我应该怎么做”正确的方式“或其他什么”

行。 但你应该以正确的方式做到......或者其他什么

  

“我需要一个如何让它阻止的具体例子......没有冻结UI。如果在JS中可以做到这一点。”

不,在不阻止UI的情况下阻止正在运行的JavaScript是不可能的。

由于缺乏信息,很难提供解决方案,但有一个选择可能是让调用函数进行一些轮询以检查全局变量,然后将回调集data设置为全局变量。 / p>

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

答案 1 :(得分:47)

看一下JQuery Promises:

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


答案 2 :(得分:43)

Async functions,一项功能in ES2017,通过使用promises(一种特殊形式的异步代码)和await关键字,使异步代码看起来同步。另请注意下面的代码示例中async关键字前面的关键字function,表示异步/等待功能。如果没有使用await关键字预先修复的功能,async关键字将无效。由于目前没有例外,这意味着没有顶级等待可以工作(顶级等待意味着等待任何功能之外)。虽然有proposal for top-level await

ES2017于2017年6月27日被批准(即最终确定)为JavaScript的标准。异步等待可能已经在您的浏览器中运行,但如果没有,您仍然可以使用j babel之类的javascript转换器或traceur。 Chrome 55完全支持异步功能。因此,如果您有更新的浏览器,您可以尝试下面的代码。

有关浏览器兼容性的信息,请参阅kangax's es2017 compatibility table

这是一个名为doAsync的async await函数示例,它占用三秒钟的暂停时间,并在每次暂停后从开始时间打印时间差:

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表达式现在被替换为unwrapped值。

所以,因为等待暂停等待然后在执行行的其余部分之前解开一个值你可以在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,因此您可以将其用作链接,就像我在上面或在另一个异步等待函数中一样。

如果您希望同时发送请求,可以使用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调用。你应该注意不要留下未处理的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)

如果你去here,你可以看到即将推出的ECMAScript版本的完成提案。

可以与ES2015(ES6)一起使用的另一种方法是使用包含发生器功能的特殊功能。生成器函数有一个yield关键字,可用于复制带有周围函数的await关键字。 yield关键字和生成器函数更通用,并且可以执行更多的事情,而不仅仅是async await函数所做的事情。如果你想要一个可用于复制异步等待的生成器函数包装器,我会查看co.js。顺便说一下co的功能很像async await函数返回一个promise。老实说,虽然此时浏览器兼容性对于生成器函数和异步函数大致相同,因此如果您只想要异步等待功能,则应使用不带co.js的异步函数。

对于IE以外的所有主流浏览器(Chrome,Safari和Edge),Async功能(截至2017年)的浏览器支持实际上非常好。

答案 3 :(得分:6)

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

答案 4 :(得分:2)

您想要的实际上现在已经可以实现。如果可以在Service Worker中运行异步代码,而在Web Worker中运行同步代码,则可以让Web Worker向Service Worker发送同步XHR,而当Service Worker执行异步操作时,Web Worker可以线程将等待。这不是一个很好的方法,但是它可以工作。

答案 5 :(得分:1)

您还可以将其转换为回调。

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

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

答案 6 :(得分:0)

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

尽管它肯定会冻结您的UI,所以对于是否可以采用所需的快捷方式,我仍然是反对者。即使NodeJS有时允许您阻止它,也无法在JavaScript中挂起“唯一线程”。在您的诺言得到解决之前,没有回调,事件,任何异步的东西都将无法处理。因此,除非读者遇到像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,因此函数和不可枚举的属性(例如原型链)将会丢失。

答案 7 :(得分:0)

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

目前:

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} 操作,则会出现运行时错误。也许这种情况表明存在另一个问题,但就这样。)

答案 8 :(得分:0)

在 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 是非常复杂和动态的平台。不可避免地有一天,这个库会突然停止工作,没有人能够对此做任何事情。

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

答案 9 :(得分:0)

使用 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));
}

答案 10 :(得分:0)

promise 的这种能力包括如下两个同步操作的关键特性(或者 then() 接受两个回调)。 得到结果后,调用resolve()并传递最终结果。 如果出错,调用reject()。

这个想法是通过 .then() 处理程序链传递结果。

const synchronize = (() => {
    let chain = Promise.resolve()
    return async (promise) => {
        return chain = chain.then(promise)
    }
})()

答案 11 :(得分:-2)

使用Async Await和Promise.resolve / Promise.all

答案 12 :(得分:-3)

通过稍微调整需求,可以实现您希望实现的想法

如果您的运行时支持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;
}