Sự khác biệt giữa `` lời hứa trả lại đang chờ đợi '' và `lời hứa trả lại '


105

Với các mẫu mã bên dưới, có sự khác biệt nào về hành vi không, và nếu có, thì những điểm khác biệt đó là gì?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

return promise

async function delay1Second() {
  return delay(1000);
}

Theo tôi hiểu, đầu tiên sẽ có xử lý lỗi trong hàm không đồng bộ và lỗi sẽ bong ra khỏi Promise của hàm không đồng bộ. Tuy nhiên, lần thứ hai sẽ yêu cầu ít đánh dấu hơn. Điều này có chính xác?

Đoạn mã này chỉ là một hàm thông thường để trả về một Lời hứa để tham khảo.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

3
Vâng, tôi đã chỉnh sửa câu hỏi của mình vì bạn hiểu sai ý của tôi và nó không thực sự trả lời những gì tôi đang thắc mắc.
PitaJ

1
@PitaJ: Tôi tin rằng ý của bạn là xóa cái asynckhỏi ( return promise) mẫu thứ hai của bạn .
Stephen Cleary

1
@PitaJ: Trong trường hợp đó, ví dụ thứ hai của bạn sẽ trả về một lời hứa được giải quyết bằng một lời hứa. Khá kỳ quặc.
Stephen Cleary

5
jakearchibald.com/2017/await-vs-return-vs-return-await là một bài viết tốt đẹp mà tóm tắt sự khác
sanchit

2
@StephenCleary, tôi tình cờ phát hiện ra điều này và lần đầu tiên tôi nghĩ chính xác như vậy, một lời hứa được giải quyết bằng một lời hứa không có ý nghĩa ở đây. Nhưng khi nó quay, promise.then(() => nestedPromise)sẽ phẳng và "theo" nestedPromise. Điều thú vị là nó khác với các tác vụ lồng nhau trong C # ở chỗ chúng ta phải làm Unwrapnhư thế nào. Một lưu ý nhỏ, có vẻ như await somePromise các cuộc gọi Promise.resolve(somePromise).then, thay vì chỉ somePromise.then, có một số khác biệt ngữ nghĩa thú vị.
mũi

Câu trả lời:


151

Hầu hết thời gian, không có sự khác biệt có thể quan sát được giữa returnreturn await. Cả hai phiên bản của delay1Secondđều có cùng một hành vi quan sát được (nhưng tùy thuộc vào việc triển khai, return awaitphiên bản có thể sử dụng nhiều bộ nhớ hơn một chút vì một Promiseđối tượng trung gian có thể được tạo).

Tuy nhiên, như @PitaJ đã chỉ ra, có một trường hợp có sự khác biệt: nếu returnhoặc return awaitđược lồng trong một try- catchkhối. Hãy xem xét ví dụ này

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

Trong phiên bản đầu tiên, hàm async chờ lời hứa bị từ chối trước khi trả về kết quả của nó, điều này khiến việc từ chối bị biến thành một ngoại lệ và catchđiều khoản cần đạt được; do đó, hàm sẽ trả về một lời hứa phân giải thành chuỗi "Đã lưu!".

Tuy nhiên, phiên bản thứ hai của hàm trả về lời hứa bị từ chối mà không cần đợi nó trong hàm async , có nghĩa là catchtrường hợp không được gọi và thay vào đó người gọi sẽ nhận được lời từ chối.


Cũng có thể đề cập rằng dấu vết ngăn xếp sẽ khác (ngay cả khi không thử / bắt)? Tôi nghĩ đó là những người vấn đề chạy vào thường xuyên nhất trong ví dụ này:]
Benjamin Gruenbaum

Tôi đã tìm thấy trong một tình huống, rằng sử dụng return new Promise(function(resolve, reject) { })trong for...ofvòng lặp và sau đó gọi resolve()trong vòng lặp sau khi pipe()không tạm dừng thực hiện chương trình cho đến khi đường ống hoàn thành, như mong muốn, tuy nhiên sử dụng await new Promise(...)thì không. cú pháp sau có hợp lệ / đúng không? nó là 'viết tắt' cho return await new Promise(...)? bạn có thể giúp tôi hiểu tại sao cái sau hoạt động và cái trước thì không? cho bối cảnh, kịch bản là trong solution 02các câu trả lời này
user1063287

10

Như các câu trả lời khác đã đề cập, có thể sẽ có một chút lợi ích về hiệu suất khi để lời hứa nổi bong bóng bằng cách trả lại trực tiếp - đơn giản vì bạn không cần phải đợi kết quả trước rồi lại kết thúc bằng một lời hứa khác. Tuy nhiên, chưa ai nói về tối ưu hóa cuộc gọi đuôi .

Tối ưu hóa cuộc gọi đuôi , hoặc "lệnh gọi đuôi thích hợp" , là một kỹ thuật mà trình thông dịch sử dụng để tối ưu hóa ngăn xếp cuộc gọi. Hiện tại, chưa có nhiều thời gian chạy hỗ trợ nó - mặc dù về mặt kỹ thuật nó là một phần của Tiêu chuẩn ES6 - nhưng hỗ trợ có thể được thêm vào trong tương lai, vì vậy bạn có thể chuẩn bị cho điều đó bằng cách viết mã tốt trong hiện tại.

Tóm lại, TCO (hoặc PTC) tối ưu hóa ngăn xếp cuộc gọi bằng cách không mở khung mới cho một hàm được trả về trực tiếp bởi một hàm khác. Thay vào đó, nó sử dụng lại cùng một khung.

async function delay1Second() {
  return delay(1000);
}

delay()được trả về trực tiếp bởi delay1Second(), các thời gian chạy hỗ trợ PTC trước tiên sẽ mở một khung cho delay1Second()(chức năng bên ngoài), nhưng sau đó thay vì mở một khung khác cho delay()(chức năng bên trong), nó sẽ chỉ sử dụng lại cùng một khung đã được mở cho chức năng bên ngoài. Điều này tối ưu hóa ngăn xếp vì nó có thể ngăn chặn tràn ngăn xếp (hehe) với các hàm đệ quy rất lớn, ví dụ fibonacci(5e+25),. Về cơ bản, nó trở thành một vòng lặp, nhanh hơn nhiều.

PTC chỉ được bật khi chức năng bên trong được trả về trực tiếp . Nó không được sử dụng khi kết quả của hàm bị thay đổi trước khi nó được trả về, chẳng hạn như nếu bạn có return (delay(1000) || null), hoặc return await delay(1000).

Nhưng như tôi đã nói, hầu hết các thời gian chạy và trình duyệt chưa hỗ trợ PTC, vì vậy nó có thể không tạo ra sự khác biệt lớn bây giờ, nhưng nó không ảnh hưởng đến việc bảo vệ mã của bạn trong tương lai.

Đọc thêm trong câu hỏi này: Node.js: Có tối ưu hóa cho các lệnh gọi đuôi trong các hàm không đồng bộ không?


2

Đây là một câu hỏi khó trả lời, bởi vì thực tế nó phụ thuộc vào cách trình chuyển đổi của bạn (có thể babel) thực sự hiển thị async/await. Những điều rõ ràng bất kể:

  • Cả hai triển khai phải hoạt động giống nhau, mặc dù triển khai đầu tiên có thể có ít hơn một Promisetrong chuỗi.

  • Đặc biệt nếu bạn bỏ những thứ không cần thiết await, phiên bản thứ hai sẽ không yêu cầu bất kỳ mã bổ sung nào từ trình chuyển tiếp, trong khi phiên bản đầu tiên thì có.

Vì vậy, từ góc độ hiệu suất mã và gỡ lỗi, phiên bản thứ hai là tốt hơn, mặc dù chỉ rất nhẹ, trong khi phiên bản đầu tiên có một chút lợi ích dễ đọc, trong đó nó chỉ ra rõ ràng rằng nó trả về một lời hứa.


Tại sao các chức năng sẽ hoạt động giống nhau? Giá trị đầu tiên trả về giá trị đã phân giải ( undefined) và giá trị thứ hai trả về a Promise.
Amit

4
@Amit cả hai chức năng trả về một Promise
PitaJ

Ack. Đây là lý do tại sao tôi không thể đứng vững async/await- tôi cảm thấy khó lý giải hơn nhiều. @PitaJ là đúng, cả hai hàm đều trả về một Lời hứa.
nrabinowitz

Điều gì sẽ xảy ra nếu tôi bao quanh phần thân của cả hai hàm không đồng bộ bằng dấu try-catch? Trong return promisetrường hợp, bất kỳ rejectionsẽ không bị bắt, chính xác, trong khi, trong return await promisetrường hợp, nó sẽ được, phải không?
PitaJ

Cả hai đều trả về một Lời hứa, nhưng lời hứa đầu tiên "hứa" một giá trị nguyên thủy và câu thứ hai "hứa ​​hẹn" một Lời hứa. Nếu bạn awaittừng người trong số này tại một số trang web cuộc gọi, kết quả sẽ rất khác nhau.
Amit

0

ở đây tôi để lại một số mã thiết thực để bạn có thể thực hiện và nó có thể làm chậm lại

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

hàm "x" chỉ là một hàm không đồng bộ hơn nó có fucn khác nếu nó sẽ xóa trả về nó in "thêm mã ..."

biến x chỉ là một hàm không đồng bộ đến lượt nó lại có một hàm không đồng bộ khác, trong phần chính của đoạn mã, chúng ta gọi hàm đợi để gọi hàm của biến x, khi nó hoàn thành, nó tuân theo trình tự của mã, điều đó sẽ bình thường cho "async / await", nhưng bên trong hàm x có một hàm không đồng bộ khác và điều này trả về một lời hứa hoặc trả về một "lời hứa" nó sẽ ở bên trong hàm x, quên mã chính, tức là nó sẽ không in "console.log (" thêm mã .. "), mặt khác nếu chúng ta đặt" await ", nó sẽ đợi mọi hàm hoàn thành và cuối cùng tuân theo trình tự bình thường của mã chính.

bên dưới "console.log (" hoàn thành 1 "xóa" trở lại ", bạn sẽ thấy hành vi.


1
Mặc dù mã này có thể giải quyết câu hỏi, bao gồm giải thích về cách thức và lý do tại sao điều này giải quyết vấn đề sẽ thực sự giúp cải thiện chất lượng bài đăng của bạn và có thể dẫn đến nhiều phiếu bầu hơn. Hãy nhớ rằng bạn đang trả lời câu hỏi cho độc giả trong tương lai, không chỉ người hỏi bây giờ. Vui lòng chỉnh sửa câu trả lời của bạn để thêm giải thích và đưa ra dấu hiệu về những giới hạn và giả định áp dụng.
Brian

0

Đây là một ví dụ về bảng chữ mà bạn có thể chạy và thuyết phục bản thân rằng bạn cần "sự trở lại đang chờ đợi"

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});


0

Sự khác biệt đáng chú ý: Việc từ chối lời hứa được xử lý ở những nơi khác nhau

  • return somePromisesẽ chuyển somePromise đến địa chỉ cuộc gọi, và await somePromise để giải quyết tại địa chỉ cuộc gọi (nếu có). Do đó, nếu somePromise bị từ chối, nó sẽ không được xử lý bởi khối bắt cục bộ mà là khối bắt của trang web cuộc gọi.

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'

  • return await somePromisetrước tiên sẽ chờ một sốPromise giải quyết tại địa phương. Do đó, giá trị hoặc Ngoại lệ trước tiên sẽ được xử lý cục bộ. => Khối bắt cục bộ sẽ được thực thi nếu somePromisebị từ chối.

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

Lý do: return await Promisechờ cả trong và ngoài, return Promisechỉ chờ bên ngoài

Các bước chi tiết:

trả lại lời hứa

async function delay1Second() {
  return delay(1000);
}
  1. cuộc gọi delay1Second();
const result = await delay1Second();
  1. Bên trong delay1Second(), hàm delay(1000)trả về một lời hứa ngay lập tức với [[PromiseStatus]]: 'pending. Hãy gọi nó delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Các hàm không đồng bộ sẽ bọc giá trị trả về của chúng bên trong Promise.resolve()( Nguồn ). Vì delay1Secondlà một hàm không đồng bộ, chúng ta có:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise)trả về delayPromisemà không làm bất cứ điều gì vì đầu vào đã là một lời hứa (xem MDN Promise.resolve ):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. awaitđợi cho đến khi delayPromiseđược giải quyết.
  • IF delayPromiseđược đáp ứng với PromiseValue = 1:
const result = 1; 
  • ELSE delayPromisebị từ chối:
// jump to catch block if there is any

trở lại đang chờ Lời hứa

async function delay1Second() {
  return await delay(1000);
}
  1. cuộc gọi delay1Second();
const result = await delay1Second();
  1. Bên trong delay1Second(), hàm delay(1000)trả về một lời hứa ngay lập tức với [[PromiseStatus]]: 'pending. Hãy gọi nó delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Sự chờ đợi của địa phương sẽ đợi cho đến khi delayPromisegiải quyết xong.
  • Trường hợp 1 : delayPromiseđược thực hiện với PromiseValue = 1:
async function delay1Second() {
  return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1; 
  • Trường hợp 2 : delayPromisebị từ chối:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;

Bảng chú giải:

  • Settle: Promise.[[PromiseStatus]]thay đổi từ pendingđến resolvedhoặcrejected
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.