Виклик асинхронної функції Javascript синхронно


222

По-перше, це дуже специфічний випадок, коли це робиться не так, щоб спеціально змінити асинхронний виклик у дуже синхронну базу даних коду, яка триває багато тисяч рядків, і час наразі не дає можливості вносити зміни в "робити" це правильно ". Це шкодить кожній клітковині мого буття, але реальність та ідеали часто не збиваються. Я знаю, що це відстійно.

Гаразд, що з дороги, як це зробити, щоб я міг:

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
Зробити блок браузера та зачекати просто неможливо. Вони просто не зроблять цього.
Pointy

2
javascript-дозант, що має механізми блокування у більшості браузерів ... ви захочете створити зворотний виклик, який викликається, коли виклик асинхронізу закінчує повернення даних
Nadir Muzaffar

8
Ви запитуєте спосіб сказати браузеру "Я знаю, що я щойно сказав вам запустити цю попередню функцію асинхронно, але я насправді цього не мав на увазі!". Чому ви навіть очікували, що це стане можливим?
Уейн

2
Дякую Дену за редагування. Я не був суворо грубим, але ваше формулювання краще.
Роберт Ч. Барт

2
@ RobertC.Barth Зараз це можливо і з JavaScript. Функції очікування async ще не ратифіковані в стандарті, але планується бути в 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 не завершить виклик до функції зворотного дзвінка. Бле, це не можна зробити, як я підозрював, мені просто потрібна була зібрана мудрість Інтернетів, щоб підтримати мене. Дякую. :-)
Роберт К. Барт

2
@ RobertC.Barth: Так, ваші підозри були, на жаль, правильними.

Це я чи тільки робота "виконана правильно"? Питання включало зворотний дзвінок, перед яким слід щось, що чекає завершення виклику асинхронізації, що ця перша частина цієї відповіді не охоплює ...
ravemir

@ravemir: У відповіді зазначено, що робити неможливо те, що він хоче. Це важлива частина, яку потрібно зрозуміти. Іншими словами, ви не можете здійснити асинхронний виклик і повернути значення, не блокуючи інтерфейс користувача. Отже, перше рішення - це некрасивий злом, який використовує глобальну змінну та опитування, щоб побачити, чи змінена ця змінна. Друга версія - правильний шлях.

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

60

Функції асинхронізації , функція в ES2017 , дозволяють асинхронному коду виглядати синхронізуванням за допомогою обіцянок (особливої ​​форми коду асинхронізації) та awaitключового слова. Також зауважте в прикладах коду нижче ключового слова asyncперед functionключовим словом, яке означає функцію асинхронізації / очікування. awaitКлючове слово не працюватиме , не будучи в функції попереднього фіксованою з asyncключовим словом. Оскільки наразі це не виняток, це означає, що очікування верхнього рівня не буде працювати (верхній рівень очікує, тобто означає очікування поза будь-якої функції). Хоча є пропозиція щодо вищого рівняawait .

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

Дивіться таблицю сумісності kangax 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();

Коли ключове слово очікування буде розміщене перед значенням обіцянки (у цьому випадку значення обіцянки - це значення, повернене функцією doSomethingAsync), ключове слово очікує призупинення виконання виклику функції, але воно не зупинить жодних інших функцій, і воно продовжиться виконання іншого коду, поки обіцянка не вирішиться. Після того, як обіцянка вирішиться, вона зніме значення обіцянки, і ви можете подумати про очікування та вираз обіцянки, як тепер замінене цим розгорнутим значенням.

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

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

Якщо обіцянка, можливо, відхиляється, ви можете зафіксувати її в спробі лову або пропустити спробу влову та дозволити помилці розповсюджуватися на функцію асинхронного / очікуваного виклику. Ви повинні бути обережними, щоб не залишати помилок обіцянок не обробленими, особливо в 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), є використання спеціальної функції, яка охоплює функцію генератора. Функції генератора мають ключове слово "вихід", яке може використовуватися для тиражування ключового слова, що очікує, з оточуючою функцією. Ключове слово і функція генератора мають набагато більш загальне призначення і можуть робити багато інших речей, аніж те, що робить функція очікування асинхронізації. Якщо ви хочете , функція пакувальник генератор , який може бути використаний для реплікації асинхронного чекати , я хотів би перевірити co.js . До речі, функція co, подібно до функції асинхронізації, що очікує, повертає обіцянку. Чесно кажучи, хоча на даний момент сумісність браузера приблизно однакова як для функцій генератора, так і для функцій асинхронізації, тому якщо ви просто хочете, щоб функція async очікувала, ви повинні використовувати функції Async без co.js.

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


2
Мені подобається ця відповідь
ycomp

1
як далеко ми дійшли :)
Дерек

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

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

Це чудова відповідь +1 і все, але написане так, як є, я не бачу, як це не менш складно, ніж використання зворотних дзвінків.
Альтім Прем'єр,

47

Погляньте на обіцянки JQuery:

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

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

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

Код Refactor:

    var dfd = новий jQuery.Deferred ();


    функція callBack (дані) {
       dfd.повідомляти (дані);
    }

    // зробіть виклик асинхронізації.
    myAsynchronousCall (param1, callBack);

    функція doSomething (дані) {
     // робити речі з даними ...
    }

    $ .when (dfd) .then (doSomething);



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 дозволяє іноді її блокувати. Ніяких зворотних дзвінків, подій і нічого асинхронного взагалі не вдасться обробити, поки ваша обіцянка не вирішиться. Тож, якщо у вас, у читача, не виникне неминуча ситуація, як ОП (або, у моєму випадку, ви пишете прославлений скрипт оболонки без зворотних викликів, подій тощо), НЕ РОБИТИ ЦЕ!

Але ось як це зробити:

./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.