Вызов асинхронной функции Javascript синхронно


223

Во-первых, это очень специфический случай неправильной реализации намеренного преобразования асинхронного вызова в очень синхронную кодовую базу длиной в несколько тысяч строк, и время в настоящее время не позволяет вносить изменения в «делать». это правильно. " Это ранит каждую клеточку моего существа, но реальность и идеалы часто не совпадают. Я знаю это отстой.

Хорошо, что с дороги, как мне сделать так, чтобы я мог:

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 до тех пор, пока не будет вызван обратный вызов) БЕЗ зависания интерфейса. Если такое возможно в JS.


16
Просто невозможно заблокировать браузер и ждать. Они просто не будут этого делать.
Заостренный

2
javascript не имеет блокирующих механизмов в большинстве браузеров ... вы захотите создать обратный вызов, который вызывается, когда асинхронный вызов завершается, чтобы вернуть данные
Надир Музаффар

8
Вы спрашиваете способ сказать браузеру: «Я знаю, я только что сказал вам, чтобы вы выполняли эту предыдущую функцию асинхронно, но на самом деле я не это имел в виду!». Почему вы ожидаете, что это будет возможно?
Уэйн

2
Спасибо Дэну за редактирование. Я не был строго груб, но ваша формулировка лучше.
Роберт С. Барт

2
@ RobertC.Barth Теперь возможно и с JavaScript. Функции асинхронного ожидания еще не ратифицированы в стандарте, но запланированы на ES2017. Смотрите мой ответ ниже для более подробной информации.
Джон

Ответы:


135

«Не говорите мне о том, как я должен делать это« правильным образом »или как-то еще»

ХОРОШО. но вы должны действительно сделать это правильно ... или что-то

«Мне нужен конкретный пример того, как заставить его блокироваться ... БЕЗ замораживания интерфейса. Если такое возможно в JS».

Нет, невозможно заблокировать работающий 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);

22
Да, я знаю, как это сделать правильно, мне нужно знать, как / если это можно сделать неправильно по конкретной причине, указанной. Суть в том, что я не хочу оставлять doSomething (), пока myAsynchronousCall не завершит вызов функции обратного вызова. Bleh, это не может быть сделано, как я подозревал, мне просто нужно собрать мудрость Интернета, чтобы поддержать меня. Спасибо. :-)
Роберт С. Барт

2
@ RobertC.Barth: Да, к сожалению, ваши подозрения были верны.

Это я или только "сделано правильно" версия работает? Вопрос включал ответный вызов, перед которым должно было что-то, что ожидает завершения асинхронного вызова, что эта первая часть этого ответа не охватывает ...
ravemir

@ravemir: в ответе говорится, что невозможно делать то, что он хочет. Это важная часть, чтобы понять. Другими словами, вы не можете сделать асинхронный вызов и вернуть значение, не блокируя пользовательский интерфейс. Таким образом, первое решение - уродливый взлом, использующий глобальную переменную и опрос, чтобы увидеть, была ли изменена эта переменная. Вторая версия - правильный путь.

1
@ Леонардо: это таинственная функция, вызываемая в вопросе. В основном это представляет все, что выполняет код асинхронно и производит результат, который должен быть получен. Так что это может быть как запрос AJAX. Вы передаете callbackфункцию myAsynchronousCallфункции, которая выполняет асинхронные операции и вызывает обратный вызов, когда завершается. Вот демо.

62

Асинхронные функции , функция в ES2017 , делают асинхронный код синхронизированным с помощью обещаний (особая форма асинхронного кода) и awaitключевого слова. Также обратите внимание на примеры кода ниже ключевого слова asyncперед functionключевым словом, которое обозначает функцию async / await. awaitКлючевое слово не будет работать , не будучи в функции предварительного фиксированной с asyncключевым словом. Поскольку в настоящее время нет исключения из этого, это означает, что не будет работать ожидание верхнего уровня (ожидание верхнего уровня означает ожидание вне какой-либо функции). Хотя есть предложение на высшем уровнеawait .

ES2017 был утвержден (то есть окончательно доработан) как стандарт для JavaScript 27 июня 2017 года. Async await может уже работать в вашем браузере, но если нет, вы все равно можете использовать эту функциональность с помощью транспортера javascript, такого как babel или traceur . Chrome 55 имеет полную поддержку асинхронных функций. Так что если у вас более новый браузер, вы можете попробовать код ниже.

См . Таблицу совместимости с кангаксом es2017 для совместимости с браузером.

Вот пример вызываемой функции асинхронного ожидания, 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 приостанавливает выполнение вызова функции, но не приостанавливает другие функции и продолжает выполнение другого кода до разрешения обещания. После того, как обещание разрешено, оно развернет значение обещания, и вы можете думать, что выражение ожидания и обещания теперь заменяется этим развернутым значением.

Итак, поскольку 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)
})

Сама асинхронная функция возвращает обещание, так что вы можете использовать его как обещание с цепочкой, как я делал выше или в другой функции асинхронного ожидания.

Приведенная выше функция будет ожидать каждого ответа перед отправкой другого запроса, если вы хотите отправлять запросы одновременно, вы можете использовать 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)
})

Если обещание может быть отклонено, вы можете заключить его в попытку catch или пропустить попытку try и позволить ошибке распространиться на вызов catch функций async / await. Вы должны быть осторожны, чтобы не оставлять ошибки в обещаниях необработанными, особенно в 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, очень похожая на функции асинхронного ожидания, возвращает обещание. Честно говоря, на данный момент совместимость браузера примерно одинакова как для функций генератора, так и для асинхронных функций, поэтому, если вы просто хотите использовать функцию асинхронного ожидания, вы должны использовать функции Async без co.js.

Поддержка браузеров в настоящее время довольно хороша для функций Async (по состоянию на 2017 год) во всех основных современных браузерах (Chrome, Safari и Edge), кроме IE.


2
Мне нравится этот ответ
Ycomp

1
как далеко мы продвинулись :)
Дерек

3
Это отличный ответ, но для оригинальной проблемы с плакатами, я думаю, все, что она делает, это поднимает проблему на один уровень. Скажем, он превращает doSomething в асинхронную функцию с ожиданием внутри. Эта функция теперь возвращает обещание и является асинхронной, поэтому ему придется снова и снова сталкиваться с одной и той же проблемой при любых вызовах этой функции.
dpwrussell

2
@dpwrussell это правда, в коде базы ползет асинхронные функции и обещания. Лучший способ разрешить обещания от проникновения ко всему - просто написать синхронные обратные вызовы. Невозможно синхронно возвращать асинхронное значение, если вы не сделаете что-то чрезвычайно странное и противоречивое, как этот twitter.com/sebmarkbage/status/941214259505119232, чего я не делаю рекомендовать. Я добавлю правку в конец вопроса, чтобы более полно ответить на вопрос, как он был задан, а не просто ответить на заголовок.
Джон

Это отличный ответ +1 и все, но написано как есть, я не вижу, как это немного сложнее, чем использование обратных вызовов.
Altimus Prime

47

Посмотрите на Обещания 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 (данные);
    }

    // делаем асинхронный вызов
    myAsynchronousCall (param1, callBack);

    function doSomething (data) {
     // делать вещи с данными ...
    }

    $ .When (ТТС) .then (йоЗотеЬЫпд);



3
+1 за этот ответ, это правильно. Тем не менее, я бы обновил строку с dfd.notify(data)доdfd.resolve(data)
Джейсон

7
Является ли это случаем кода, дающего иллюзию синхронности, фактически не являясь асинхронным?
Сауршаз

2
обещания - это IMO, просто хорошо организованные обратные вызовы :) если вам нужен асинхронный вызов, скажем, при некоторой инициализации объекта, то обещания имеют небольшое значение.
webduvet

10
Обещания не синхронизированы.
Vans S

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

3

Вы можете заставить асинхронный JavaScript в NodeJS быть синхронным с sync-rpc .

Тем не менее, он определенно заморозит ваш пользовательский интерфейс, поэтому я все еще скептически отношусь к тому, возможно ли использовать ярлык, который вам нужен. Невозможно приостановить единый поток в JavaScript, даже если NodeJS позволяет вам иногда блокировать его. Никакие обратные вызовы, события или что-либо асинхронное вообще не смогут обрабатываться, пока ваше обещание не разрешится. Поэтому, если вы, читатель, не столкнетесь с неизбежной ситуацией, такой как OP (или, в моем случае, не пишете прославленный скрипт оболочки без обратных вызовов, событий и т. Д.), НЕ ДЕЛАЙТЕ ЭТОГО!

Но вот как вы можете это сделать:

./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будут входить и выходить , поэтому функции и неперечислимые свойства, такие как цепочки прототипов, будут потеряны.

1

Вы также можете конвертировать его в обратные вызовы.

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

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

0

То, что вы хотите, на самом деле возможно сейчас. Если вы можете запустить асинхронный код в сервисном работнике и синхронный код в веб-работнике, тогда вы можете попросить веб-работника отправить синхронный XHR сервисному работнику, и пока работник сервиса выполняет асинхронные операции, веб-работник нить будет ждать. Это не очень хороший подход, но он может сработать.


-4

Идея, которую вы надеетесь достичь, станет возможной, если вы немного измените требование

Приведенный ниже код возможен, если ваша среда выполнения поддерживает спецификацию 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;
}

4
Firefox выдает ошибку: SyntaxError: await is only valid in async functions and async generators. Не говоря уже о том, что param1 не определен (и даже не используется).
Харви
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.