Gọi một chức năng Javascript không đồng bộ


222

Đầu tiên, đây là một trường hợp rất cụ thể khi thực hiện sai mục đích nhằm đưa một cuộc gọi không đồng bộ vào một cơ sở mã rất đồng bộ dài hàng ngàn dòng và thời gian hiện không đủ khả năng để thực hiện các thay đổi thành "làm đúng." Nó làm tổn thương mọi sợi cơ thể của tôi, nhưng thực tế và lý tưởng thường không hòa hợp. Tôi biết điều này hút.

Được rồi, ngoài cách đó, làm thế nào để tôi có thể:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

Các ví dụ (hoặc thiếu chúng) đều sử dụng các thư viện và / hoặc trình biên dịch, cả hai đều không khả thi cho giải pháp này. Tôi cần một ví dụ cụ thể về cách làm cho nó chặn (ví dụ: KHÔNG rời khỏi hàm doS Something cho đến khi gọi lại) mà KHÔNG đóng băng UI. Nếu điều đó là có thể trong JS.


16
Đơn giản là không thể tạo khối trình duyệt và chờ đợi. Họ sẽ không làm điều đó.
Mũi nhọn

2
Công cụ javascript có cơ chế chặn trên hầu hết các trình duyệt ... bạn sẽ muốn tạo một cuộc gọi lại được gọi khi cuộc gọi async kết thúc để trả về dữ liệu
Nadir Muzaffar

8
Bạn đang yêu cầu một cách để nói với trình duyệt "Tôi biết tôi chỉ bảo bạn chạy chức năng trước đó một cách không đồng bộ, nhưng tôi không thực sự có ý đó!". Tại sao bạn thậm chí mong đợi điều đó là có thể?
Wayne

2
Cảm ơn Dan đã chỉnh sửa. Tôi thực sự không thô lỗ, nhưng từ ngữ của bạn tốt hơn.
Robert C. Barth

2
@ RobertC.Barth Bây giờ cũng có thể với JavaScript. Các chức năng chờ đồng bộ chưa được phê chuẩn trong tiêu chuẩn, nhưng được lên kế hoạch trong ES2017. Xem câu trả lời của tôi dưới đây để biết thêm chi tiết.
John

Câu trả lời:


135

"đừng nói với tôi về cách tôi nên làm" đúng cách "hay bất cứ điều gì"

ĐỒNG Ý. nhưng bạn thực sự nên làm điều đó đúng cách ... hoặc bất cứ điều gì

"Tôi cần một ví dụ cụ thể về cách làm cho nó bị chặn ... MÀ KHÔNG đóng băng UI. Nếu điều đó là có thể trong JS."

Không, không thể chặn JavaScript đang chạy mà không chặn UI.

Do thiếu thông tin, thật khó để đưa ra giải pháp, nhưng một lựa chọn có thể là chức năng gọi thực hiện một số bỏ phiếu để kiểm tra biến toàn cục, sau đó đặt lại cuộc gọi thành datatoàn cầu.

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

Tất cả điều này giả định rằng bạn có thể sửa đổi doSomething(). Tôi không biết nếu đó là trong thẻ.

Nếu nó có thể được sửa đổi, thì tôi không biết tại sao bạn sẽ không chuyển một cuộc gọi lại doSomething()để được gọi từ cuộc gọi lại khác, nhưng tốt nhất tôi nên dừng lại trước khi tôi gặp rắc rối. ;)


Ôi, cái quái gì thế. Bạn đã đưa ra một ví dụ cho thấy nó có thể được thực hiện chính xác, vì vậy tôi sẽ trình bày giải pháp đó ...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

doSomething(function(data) {
    console.log(data);
});

Vì ví dụ của bạn bao gồm một cuộc gọi lại được chuyển đến cuộc gọi không đồng bộ, nên cách đúng sẽ là chuyển một hàm doSomething()được gọi từ cuộc gọi lại.

Tất nhiên, nếu đó là điều duy nhất mà cuộc gọi lại đang thực hiện, bạn sẽ vượt qua functrực tiếp ...

myAsynchronousCall(param1, func);

22
Vâng, tôi biết làm thế nào để làm điều đó một cách chính xác, tôi cần biết làm thế nào để / nếu nó có thể được thực hiện không chính xác cho lý do cụ thể được nêu. Điểm mấu chốt là tôi không muốn rời doS Something () cho đến khi myAsynyncousCall hoàn thành cuộc gọi đến chức năng gọi lại. Bleh, điều đó không thể được thực hiện, vì tôi nghi ngờ, tôi chỉ cần sự khôn ngoan thu thập được của các Internets để hỗ trợ tôi. Cảm ơn bạn. :-)
Robert C. Barth

2
@ RobertC.Barth: Vâng, sự nghi ngờ của bạn là không may.

Là tôi hay chỉ phiên bản "thực hiện chính xác" hoạt động? Câu hỏi bao gồm một cuộc gọi trở lại, trước đó sẽ có một cuộc gọi chờ cuộc gọi không đồng bộ kết thúc, phần đầu tiên của câu trả lời này không bao gồm ...
ravemir

@ravemir: Câu trả lời nói rằng không thể làm những gì anh ấy muốn. Đó là phần quan trọng để hiểu. Nói cách khác, bạn không thể thực hiện cuộc gọi không đồng bộ và trả về giá trị mà không chặn UI. Vì vậy, giải pháp đầu tiên là một hack xấu xí sử dụng biến toàn cục và bỏ phiếu để xem biến đó có bị sửa đổi hay không. Phiên bản thứ hai là cách chính xác.

1
@Leonardo: Đó là chức năng bí ẩn được gọi trong câu hỏi. Về cơ bản, nó đại diện cho bất cứ điều gì chạy mã không đồng bộ và tạo ra một kết quả cần phải nhận được. Vì vậy, nó có thể giống như một yêu cầu AJAX. Bạn chuyển callbackhàm cho myAsynchronousCallhàm, hàm này thực hiện công cụ không đồng bộ của nó và gọi lại khi hoàn thành. Đây là một bản demo.

60

Các chức năng async , một tính năng trong ES2017 , làm cho mã async trông đồng bộ hóa bằng cách sử dụng các lời hứa (một dạng mã async cụ thể) và awaittừ khóa. Cũng lưu ý trong các ví dụ mã bên dưới từ khóa asyncphía trước functiontừ khóa biểu thị chức năng async / await. Các awaittừ khóa sẽ không hoạt động mà không bị trong một chức năng trước cố định với các asynctừ khóa. Vì hiện tại không có ngoại lệ cho điều này có nghĩa là không có sự chờ đợi cấp cao nhất sẽ hoạt động (cấp cao nhất đang chờ có nghĩa là chờ đợi bên ngoài bất kỳ chức năng nào). Mặc dù có một đề xuất cho cấp cao nhấtawait .

ES2017 được phê chuẩn (tức là đã quyết toán) như là tiêu chuẩn cho JavaScript trên 27 tháng 6, 2017. Async chờ đợi có thể đã làm việc trong trình duyệt của bạn, nhưng nếu không bạn vẫn có thể sử dụng các chức năng sử dụng một transpiler javascript như babel hoặc Traceur . Chrome 55 có hỗ trợ đầy đủ các chức năng không đồng bộ. Vì vậy, nếu bạn có một trình duyệt mới hơn, bạn có thể thử mã bên dưới.

Xem bảng tương thích es2017 của kangax để biết khả năng tương thích trình duyệt.

Dưới đây là một ví dụ về chức năng chờ đồng bộ được gọi là doAsynctạm dừng ba giây và in chênh lệch thời gian sau mỗi lần tạm dừng từ thời gian bắt đầu:

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

Khi từ khóa await được đặt trước một giá trị lời hứa (trong trường hợp này, giá trị lời hứa là giá trị được trả về bởi hàm doS SomethingAsync), từ khóa await sẽ tạm dừng thực hiện lệnh gọi hàm, nhưng nó sẽ không tạm dừng bất kỳ chức năng nào khác và nó sẽ tiếp tục thực thi mã khác cho đến khi lời hứa được giải quyết. Sau khi lời hứa được giải quyết, nó sẽ mở ra giá trị của lời hứa và bạn có thể nghĩ đến biểu hiện chờ đợi và lời hứa như bây giờ được thay thế bằng giá trị chưa được tiết lộ đó.

Vì vậy, vì chờ đợi chỉ tạm dừng chờ đợi để mở ra một giá trị trước khi thực hiện phần còn lại của dòng, bạn có thể sử dụng nó cho các vòng lặp và bên trong các lệnh gọi như trong ví dụ dưới đây thu thập sự khác biệt về thời gian chờ trong một mảng và in ra mảng.

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

Chính hàm async trả về một lời hứa để bạn có thể sử dụng lời hứa đó với chuỗi như tôi thực hiện ở trên hoặc trong một hàm chờ async khác.

Hàm trên sẽ đợi từng phản hồi trước khi gửi yêu cầu khác nếu bạn muốn gửi yêu cầu đồng thời, bạn có thể sử dụng 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)
})

Nếu lời hứa có thể từ chối, bạn có thể gói nó trong một thử bắt hoặc bỏ qua thử bắt và để lỗi lan truyền đến hàm async / await các chức năng bắt cuộc gọi. Bạn nên cẩn thận để không xử lý các lỗi hứa hẹn, đặc biệt là trong Node.js. Dưới đây là một số ví dụ cho thấy cách làm việc lỗi.

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)

Nếu bạn đến đây, bạn có thể thấy các đề xuất đã hoàn thành cho các phiên bản ECMAScript sắp tới.

Một thay thế cho điều này có thể được sử dụng chỉ với ES2015 (ES6) là sử dụng một chức năng đặc biệt bao bọc một chức năng của trình tạo. Các hàm tạo có một từ khóa năng suất có thể được sử dụng để sao chép từ khóa đang chờ với một hàm xung quanh. Từ khóa năng suất và chức năng tạo là mục đích chung hơn rất nhiều và có thể làm nhiều việc khác sau đó chỉ là chức năng chờ đợi không đồng bộ. Nếu bạn muốn có một chức năng máy phát điện wrapper mà có thể được sử dụng để async lặp chờ đợi tôi sẽ kiểm tra co.js . Bằng cách này, chức năng của co giống như async đang chờ các hàm trả lại một lời hứa. Thành thật mà nói, tại thời điểm này khả năng tương thích của trình duyệt gần giống nhau cho cả chức năng trình tạo và chức năng không đồng bộ, vì vậy nếu bạn chỉ muốn chức năng chờ đồng bộ, bạn nên sử dụng các chức năng Async mà không có co.js.

Hiện tại, hỗ trợ trình duyệt thực sự khá tốt đối với các chức năng Async (kể từ năm 2017) trong tất cả các trình duyệt chính hiện tại (Chrome, Safari và Edge) ngoại trừ IE.


2
Tôi thích câu trả lời này
ycomp

1
chúng ta đã đi bao xa :)
Derek

3
Đây là một câu trả lời tuyệt vời, nhưng đối với vấn đề áp phích ban đầu, tôi nghĩ tất cả những gì nó làm là đưa vấn đề lên một cấp độ. Nói rằng anh ta biến doS Something thành một chức năng không đồng bộ với sự chờ đợi bên trong. Hàm đó bây giờ trả về một lời hứa và không đồng bộ, vì vậy anh ta sẽ phải xử lý cùng một vấn đề một lần nữa trong bất kỳ cuộc gọi nào với chức năng đó.
dpwrussell

1
@dpwrussell điều này là đúng, có một chuỗi các hàm async và lời hứa trong cơ sở mã. Cách tốt nhất để giải quyết các lời hứa từ việc leo vào mọi thứ chỉ là viết các cuộc gọi lại đồng bộ, không có cách nào để trả về giá trị async một cách đồng bộ trừ khi bạn làm điều gì đó cực kỳ kỳ lạ và gây tranh cãi như twitter.com/sebmarkbage/status/941214259505119232 mà tôi không làm giới thiệu. Tôi sẽ thêm một chỉnh sửa vào cuối câu hỏi để trả lời đầy đủ hơn câu hỏi như nó đã được hỏi và không chỉ trả lời tiêu đề.
Giăng

Đó là một câu trả lời tuyệt vời +1 và tất cả, nhưng được viết như vậy, tôi không thấy điều này ít phức tạp hơn việc sử dụng các cuộc gọi lại.
Altimus Prime

47

Hãy xem Lời hứa của JQuery:

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

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

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

Tái cấu trúc mã:

    var dfd = new jQuery.Deferred ();


    chức năng gọi lại (dữ liệu) {
       dfd.notify (dữ liệu);
    }

    // thực hiện cuộc gọi async.
    myAsynyncousCall (param1, callBack);

    hàm doS Something (data) {
     // làm công cụ với dữ liệu ...
    }

    $ .when (dfd) .then (doS Something);



3
+1 cho câu trả lời này, điều này là chính xác. Tuy nhiên, tôi sẽ cập nhật phù hợp với dfd.notify(data)tớidfd.resolve(data)
Jason

7
Đây có phải là một trường hợp mã tạo ảo giác là đồng bộ, mà không thực sự KHÔNG đồng bộ?
saurshaz

2
lời hứa là IMO chỉ là các cuộc gọi lại được tổ chức tốt :) nếu bạn cần một cuộc gọi không đồng bộ trong giả sử một số khởi tạo đối tượng, hơn là những lời hứa tạo ra một chút khác biệt.
webduvet

10
Lời hứa không đồng bộ.
Vans S

6

Có một cách giải quyết tốt đẹp tại http://taskjs.org/

Nó sử dụng các trình tạo mới cho javascript. Vì vậy, nó hiện không được thực hiện bởi hầu hết các trình duyệt. Tôi đã thử nghiệm nó trong firefox và đối với tôi, đây là cách hay để bọc chức năng không đồng bộ.

Dưới đây là mã ví dụ từ dự án 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

Bạn có thể buộc JavaScript không đồng bộ trong NodeJS phải đồng bộ hóa với sync-rpc .

Mặc dù vậy, nó chắc chắn sẽ đóng băng giao diện người dùng của bạn, vì vậy tôi vẫn là một người ít nói khi biết liệu có thể dùng phím tắt nào bạn cần thực hiện hay không. Không thể tạm dừng Chủ đề Một và Chỉ trong JavaScript, ngay cả khi NodeJS đôi khi cho phép bạn chặn nó. Không có cuộc gọi lại, sự kiện, bất cứ điều gì không đồng bộ sẽ có thể xử lý cho đến khi lời hứa của bạn được giải quyết. Vì vậy, trừ khi bạn đọc có một tình huống không thể tránh khỏi như OP (hoặc, trong trường hợp của tôi, đang viết một kịch bản shell được tôn vinh mà không có cuộc gọi lại, sự kiện, v.v.), ĐỪNG LÀM ĐIỀU NÀY!

Nhưng đây là cách bạn có thể làm điều này:

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

GIỚI HẠN:

Cả hai đều là hậu quả của cách sync-rpcthực hiện, đó là bằng cách lạm dụng require('child_process').spawnSync:

  1. Điều này sẽ không hoạt động trong trình duyệt.
  2. Các đối số cho chức năng của bạn phải được tuần tự hóa. Đối số của bạn sẽ truyền vào và ra JSON.stringify, do đó, các hàm và thuộc tính không thể đếm được như chuỗi nguyên mẫu sẽ bị mất.

1

Bạn cũng có thể chuyển đổi nó thành cuộc gọi lại.

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

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

0

Những gì bạn muốn là thực sự có thể bây giờ. Nếu bạn có thể chạy mã không đồng bộ trong một nhân viên dịch vụ và mã đồng bộ trong một nhân viên web, thì bạn có thể yêu cầu nhân viên web gửi một XHR đồng bộ cho nhân viên dịch vụ và trong khi nhân viên dịch vụ thực hiện các việc không đồng bộ, thì nhân viên web Chủ đề sẽ chờ. Đây không phải là một cách tiếp cận tuyệt vời, nhưng nó có thể làm việc.


-4

Ý tưởng mà bạn hy vọng đạt được có thể được thực hiện nếu bạn điều chỉnh yêu cầu một chút

Mã dưới đây là có thể nếu thời gian chạy của bạn hỗ trợ đặc tả ES6.

Tìm hiểu thêm về các chức năng không đồng bộ

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 đưa ra lỗi : SyntaxError: await is only valid in async functions and async generators. Chưa kể rằng param1 không được xác định (và thậm chí không được sử dụng).
Harvey
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.