同步调用异步 JavaScript 函数

Call An Asynchronous Javascript Function Synchronously

本文关键字:函数 JavaScript 异步 调用 同步      更新时间:2023-09-26

首先,这是一个非常特殊的情况,故意以错误的方式执行此操作,将异步调用改装为数千行长的非常同步的代码库,并且时间目前无法进行更改以"做对"。它伤害了我生命的每一根纤维,但现实和理想往往不相吻合。我知道这很糟糕。

好的,既然如此

,我该如何做到这一点,以便我可以:

function doSomething() {
  var data;
  function callBack(d) {
    data = d;
  }
  myAsynchronousCall(param1, callBack);
  // block here and return data when the callback is finished
  return data;
}

示例(或缺少这些示例(都使用库和/或编译器,这两者都不适用于此解决方案。我需要一个具体的例子来说明如何在不冻结 UI 的情况下使其阻塞(例如,在调用回调之前不要离开 doSomething 函数(。如果在 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 关键字使异步代码看起来同步。另请注意,在下面的代码示例中,关键字async在表示 async/await 函数的 function 关键字前面。如果不在以 async 关键字为前缀的函数中,await 关键字将不起作用。由于目前没有例外,这意味着没有顶级等待将起作用(顶级等待意味着在任何函数之外等待(。虽然有一个顶级await的建议.

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

有关浏览器兼容性,请参阅 kangax 的 es2017 兼容性表。

下面是一个名为 doAsync 的示例异步 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 表达式视为现在被解包的值所取代。

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

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

异步

函数本身返回一个承诺,因此您可以将其用作带有链接的承诺,就像我在上面或在另一个异步 await 函数中所做的那样。

上面的函数将在发送另一个请求之前等待每个响应,如果你想同时发送请求,你可以使用 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 调用。你应该小心不要让承诺错误未处理,尤其是在 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 关键字和生成器函数是更通用的用途,可以做更多的事情,然后就是异步等待函数所做的。如果您想要一个可用于复制异步等待的生成器函数包装器,我会查看 co.js。顺便说一下,co的函数很像异步await函数返回一个承诺。老实说,虽然在这一点上浏览器对生成器函数和异步函数的兼容性大致相同,所以如果你只想要异步等待功能,你应该使用没有 co.js 的异步函数。(我建议只使用 async/await,它在支持上述删除线的大多数环境中得到了广泛的支持。

浏览器支持实际上非常好,现在除了IE之外,所有主要当前浏览器(Chrome,Safari和Edge(的异步功能(截至2017年(都非常好。

看看 JQuery Promises:

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

重构代码:

   var dfd = new jQuery.Deferred((;函数调用返回(数据( {       dfd.notify(data(;    }    执行异步调用。    myAsyncCall(param1, callBack(;    函数 doSomething(data( {     用数据做事...    }    $.when(dfd(.then(doSomething(;

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

不过,它肯定会冻结您的 UI,因此当涉及到是否可以采取您需要采取的快捷方式时,我仍然是一个反对者。在 JavaScript 中不可能挂起 One And Only Thread,即使 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;
}

你想要的现在实际上是可能的。如果可以在服务工作进程中运行异步代码,在 Web 辅助角色中运行同步代码,则可以让 Web 工作进程向服务工作进程发送同步 XHR,当服务工作进程执行异步操作时,Web 辅助角色的线程将等待。这不是一个好方法,但它可以工作。

let result;
async_function().then(r => result = r);
while (result === undefined) // Wait result from async_function
    require('deasync').sleep(100);

在 Node 中.js可以编写实际调用异步操作的同步代码。节点光纤允许这样做。它是作为 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 注释:

过时说明 - 该项目的作者建议您尽可能避免使用它。该模块的原始版本在 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'

当然,如果异步函数依赖于固有的异步操作(网络请求等(,则这是行不通的,在这种情况下,努力是徒劳的(没有有效地无缘无故地等待空闲旋转(。

此外,根据传入的选项返回值或承诺也是相当丑陋的。

("如果它不使用异步构造,我为什么要编写异步函数?"有人可能会问?也许函数的某些模式/参数需要异步性,而其他模式/参数不需要,并且由于代码重复,您需要一个整体块,而不是在不同函数中单独的模块化代码块......例如,参数可能是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));
}

这种承诺能力包括同步操作的两个关键特性,如下所示(或 then(( 接受两个回调(。当你得到结果时,调用 resolve(( 并传递最终结果。如果出现错误,请调用 reject((。

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

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

我想知道同样的事情,并注意到当前的最佳答案在我脑海中包含了大多数用例的正确想法,但忘记提及几件事。当使用全局变量来锁定执行时,我们谈论的是信号量,并且有一些包可以实现这些(我的建议:async-sema(。我认为这使它更简单,更干净。

import { Sema } from 'async-sema'
const sema = new Sema(1) // allow only one concurrent process
async function doSomething() {
  var data;
   
  await sema.acquire();
  // only one process gets inside here
  data = await myAsynchronousCall(param1);
  sema.release();
  return data;
}

优点显然是程序的其余部分仍然可以异步执行其他操作,只有单个块被强制同步。缺点是您必须小心锁定的内容和位置,尝试/捕获/最终可能的错误等。

在 Node 中.js可以使用 deasync

  • npm install --save deasync

代码示例:

function asyncToSync(promise) {
  let done = false;
  let result;
  promise.then((r) => {
    result = r;
    done = true;
  }).catch((e) => {
    console.error(e);
    done = true;
  });
  require('deasync').loopWhile(() => !done);
  return result;
}

并使用该函数:

const value = asyncToSync(Promise.resolve(3));
console.log(value) // 3

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

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