ความแตกต่างระหว่าง "คำสัญญาที่รอการส่งคืน" และ "คำสัญญาที่กลับมา"


116

จากตัวอย่างโค้ดด้านล่างมีพฤติกรรมที่แตกต่างกันหรือไม่และถ้าเป็นเช่นนั้นความแตกต่างเหล่านั้นคืออะไร?

return await promise

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

return promise

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

ตามที่ฉันเข้าใจข้อแรกจะมีการจัดการข้อผิดพลาดภายในฟังก์ชัน async และข้อผิดพลาดจะเกิดขึ้นจาก Promise ของฟังก์ชัน async อย่างไรก็ตามข้อที่สองจะต้องใช้ขีดน้อยกว่าหนึ่งขีด ถูกต้องหรือไม่

ตัวอย่างข้อมูลนี้เป็นเพียงฟังก์ชันทั่วไปในการส่งคืน Promise สำหรับการอ้างอิง

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

3
ใช่ฉันแก้ไขคำถามของฉันเพราะคุณเข้าใจความหมายของฉันผิดและมันก็ไม่ได้ตอบในสิ่งที่ฉันสงสัยจริงๆ
PitaJ

1
@PitaJ: ฉันเชื่อว่าคุณตั้งใจที่จะลบตัวอย่างasyncที่สอง ( return promise) ของคุณ
Stephen Cleary

1
@PitaJ: ในกรณีนั้นตัวอย่างที่สองของคุณจะส่งคืนคำสัญญาที่ได้รับการแก้ไขด้วยคำสัญญา ค่อนข้างแปลก
Stephen Cleary

5
jakearchibald.com/2017/await-vs-return-vs-return-awaitเป็นบทความที่ดีที่สรุปความแตกต่าง
sanchit

2
@StephenCleary ฉันสะดุดกับสิ่งนี้ครั้งแรกและคิดว่าเหมือนกันทุกประการสัญญาที่ได้รับการแก้ไขด้วยคำสัญญาไม่สมเหตุสมผลที่นี่ แต่ขณะที่มันหันpromise.then(() => nestedPromise)จะแผ่และ nestedPromise"ทำตามที่" น่าสนใจว่ามันแตกต่างจากงานที่ซ้อนกันใน C # ที่เราจะต้องUnwrapมัน ในบันทึกด้านข้างดูเหมือนว่าการ await somePromiseโทรPromise.resolve(somePromise).thenแทนที่จะเป็นเพียงแค่somePromise.thenความแตกต่างทางความหมายที่น่าสนใจ
noseratio

คำตอบ:


165

ส่วนใหญ่เวลาที่ไม่มีความแตกต่างที่สังเกตได้ระหว่างและreturn return awaitทั้งสองเวอร์ชันdelay1Secondมีลักษณะการทำงานที่สังเกตได้เหมือนกัน (แต่ขึ้นอยู่กับการนำไปใช้งานreturn awaitเวอร์ชันอาจใช้หน่วยความจำมากกว่าเล็กน้อยเนื่องจากPromiseอาจมีการสร้างอ็อบเจ็กต์กลาง)

แต่เป็น @PitaJ ชี้ให้เห็นมีกรณีหนึ่งที่มีความแตกต่าง: ถ้าreturnหรือreturn awaitซ้อนในtry- catchบล็อก ลองพิจารณาตัวอย่างนี้

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!'
  }
}

ในเวอร์ชันแรกฟังก์ชัน async จะรอคำสัญญาที่ถูกปฏิเสธก่อนที่จะส่งคืนผลลัพธ์ซึ่งจะทำให้การปฏิเสธกลายเป็นข้อยกเว้นและcatchจะต้องไปถึงอนุประโยค ฟังก์ชันนี้จะส่งคืนสัญญาที่แก้ไขเป็นสตริง "บันทึกแล้ว!"

อย่างไรก็ตามฟังก์ชันเวอร์ชันที่สองจะส่งคืนคำสัญญาที่ถูกปฏิเสธโดยตรงโดยไม่ต้องรอภายในฟังก์ชัน asyncซึ่งหมายความว่าcatchจะไม่มีการเรียกเคสและผู้โทรจะได้รับคำปฏิเสธแทน


อาจพูดถึงว่าการติดตามสแต็กจะแตกต่างกัน (แม้ว่าจะไม่ได้ลอง / จับ)? ฉันคิดว่านั่นเป็นปัญหาที่ผู้คนพบบ่อยที่สุดในตัวอย่างนี้:]
Benjamin Gruenbaum

ฉันพบในสถานการณ์หนึ่งที่ใช้return new Promise(function(resolve, reject) { })ภายในfor...ofลูปแล้วเรียกresolve()ภายในลูปหลังจากที่pipe()ไม่หยุดการทำงานของโปรแกรมชั่วคราวจนกว่าไพพ์จะเสร็จสิ้นตามที่ต้องการอย่างไรก็ตามการใช้await new Promise(...)มัน หลังเป็นไวยากรณ์ที่ถูกต้อง / ถูกต้องหรือไม่? เป็น 'ชวเลข' เพื่อreturn await new Promise(...)? คุณช่วยฉันเข้าใจได้ไหมว่าทำไมผลงานหลังและผลงานในอดีตไม่ได้ สำหรับบริบทสถานการณ์ที่อยู่ในsolution 02ของคำตอบนี้
user1063287

14

ดังคำตอบอื่น ๆ ที่กล่าวถึงมีแนวโน้มที่จะได้รับประโยชน์ด้านประสิทธิภาพเล็กน้อยเมื่อปล่อยให้คำมั่นสัญญาโดยส่งคืนโดยตรงเพียงเพราะคุณไม่ต้องรอผลลัพธ์ก่อนแล้วจึงห่อด้วยคำสัญญาอื่นอีกครั้ง แต่ไม่มีใครได้พูดคุยเกี่ยวกับการเพิ่มประสิทธิภาพการโทรหางเลย

การเพิ่มประสิทธิภาพการโทรแบบหางหรือ"การเรียกหางที่เหมาะสม"เป็นเทคนิคที่ล่ามใช้เพื่อเพิ่มประสิทธิภาพกองการโทร ขณะนี้ยังมีเวลาทำงานไม่มากนักแม้ว่าจะเป็นส่วนหนึ่งของมาตรฐาน ES6ในทางเทคนิคก็ตามแต่อาจมีการเพิ่มการสนับสนุนในอนาคตดังนั้นคุณจึงสามารถเตรียมความพร้อมได้โดยการเขียนโค้ดที่ดีในปัจจุบัน

โดยสรุป TCO (หรือ PTC) จะเพิ่มประสิทธิภาพ call stack โดยไม่เปิดเฟรมใหม่สำหรับฟังก์ชันที่ส่งคืนโดยตรงโดยฟังก์ชันอื่น แต่จะใช้กรอบเดิมซ้ำ

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

เนื่องจากdelay()ถูกส่งกลับโดยตรงโดยdelay1Second()รันไทม์ที่รองรับ PTC ก่อนจะเปิดเฟรมสำหรับdelay1Second()(ฟังก์ชันด้านนอก) แต่จากนั้นแทนที่จะเปิดเฟรมอื่นสำหรับdelay()(ฟังก์ชันด้านใน) มันจะใช้เฟรมเดิมที่เปิดไว้สำหรับฟังก์ชันด้านนอกซ้ำ สิ่งนี้จะเพิ่มประสิทธิภาพสแต็กเนื่องจากสามารถป้องกันไม่ให้สแต็กล้น (ฮิฮิ) ด้วยฟังก์ชันการเรียกซ้ำขนาดใหญ่มากเช่นfibonacci(5e+25). โดยพื้นฐานแล้วมันจะกลายเป็นลูปซึ่งเร็วกว่ามาก

PTC จะเปิดใช้งานก็ต่อเมื่อฟังก์ชันภายในถูกส่งกลับโดยตรง จะไม่ใช้เมื่อผลลัพธ์ของฟังก์ชันถูกเปลี่ยนแปลงก่อนที่จะส่งคืนตัวอย่างเช่นถ้าคุณมีreturn (delay(1000) || null)หรือreturn await delay(1000)หรือ

แต่อย่างที่ฉันพูดไปแล้วเวลาทำงานและเบราว์เซอร์ส่วนใหญ่ยังไม่รองรับ PTC ดังนั้นตอนนี้อาจไม่ได้สร้างความแตกต่างอย่างมาก แต่ก็ไม่สามารถทำร้ายรหัสของคุณในอนาคตได้

อ่านเพิ่มเติมในคำถามนี้: Node.js: มีการเพิ่มประสิทธิภาพสำหรับการเรียกหางในฟังก์ชัน async หรือไม่


3

นี่เป็นคำถามที่ยากที่จะตอบเนื่องจากในทางปฏิบัติขึ้นอยู่กับว่าทรานสไพเลอร์ของคุณ (อาจbabel) แสดงผลจริงasync/awaitอย่างไร สิ่งที่ชัดเจนโดยไม่คำนึงถึง:

  • การใช้งานทั้งสองควรทำงานเหมือนกันแม้ว่าการใช้งานครั้งแรกอาจมีน้อยกว่าหนึ่งPromiseในห่วงโซ่

  • โดยเฉพาะอย่างยิ่งถ้าคุณทิ้งสิ่งที่ไม่จำเป็นawaitเวอร์ชันที่สองจะไม่ต้องใช้รหัสพิเศษใด ๆ จากตัวส่งสัญญาณในขณะที่รุ่นแรกทำ

ดังนั้นจากประสิทธิภาพของโค้ดและมุมมองของการดีบักเวอร์ชันที่สองจึงเป็นที่นิยมมากกว่าแม้ว่าจะมีเพียงเล็กน้อยมากก็ตามในขณะที่เวอร์ชันแรกมีประโยชน์ด้านความสามารถในการอ่านได้เล็กน้อยเนื่องจากระบุอย่างชัดเจนว่าจะส่งคืนสัญญา


ทำไมฟังก์ชั่นจึงทำงานเหมือนกัน? เป็นครั้งแรกที่ผลตอบแทนคุ้มค่าการแก้ไข ( undefined) Promiseและผลตอบแทนที่สอง
Amit

4
@Amit ทั้งสองฟังก์ชั่นคืนคำสัญญา
PitaJ

อ๊าก. นี่คือสาเหตุที่ฉันยืนไม่ได้async/await- ฉันคิดว่ามันยากกว่าที่จะหาเหตุผล @PitaJ ถูกต้องทั้งสองฟังก์ชั่นส่งคืน Promise
nrabinowitz

จะเกิดอะไรขึ้นถ้าฉันล้อมรอบร่างกายของฟังก์ชัน async ทั้งสองด้วย a try-catch? ในreturn promiseกรณีใด ๆrejectionจะไม่ถูกจับได้ถูกต้องในขณะที่ในreturn await promiseกรณีนี้จะเป็นใช่ไหม?
PitaJ

ทั้งสองคืนคำสัญญา แต่คำสัญญาแรก "สัญญา" เป็นคุณค่าดั้งเดิมและคำที่สอง "สัญญา" คำสัญญา หากคุณทำawaitสิ่งเหล่านี้ในไซต์การโทรบางแห่งผลลัพธ์จะแตกต่างกันมาก
มิต

2

ความแตกต่างที่เห็นได้ชัดเจน: การปฏิเสธสัญญาจะได้รับการจัดการในสถานที่ต่างๆ

  • return somePromiseจะส่งผ่านsomePromiseไปยังไซต์การโทรและawait somePromise ที่จะชำระที่ไซต์การโทร (ถ้ามี) ดังนั้นหาก somePromise ถูกปฏิเสธจะไม่ได้รับการจัดการโดย catch block ในพื้นที่ แต่เป็นบล็อก catch ของไซต์โทร

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 somePromiseก่อนอื่นจะรอบางส่วนสัญญาว่าจะตั้งถิ่นฐานในพื้นที่ ดังนั้นค่าหรือ Exception จะถูกจัดการในเครื่องก่อน => Local catch block จะถูกดำเนินการหากsomePromiseถูกปฏิเสธ

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'

เหตุผล: return await Promiseรอทั้งในและนอกพื้นที่return Promiseรอเฉพาะด้านนอก

ขั้นตอนโดยละเอียด:

คืนสัญญา

async function delay1Second() {
  return delay(1000);
}
  1. โทรdelay1Second();
const result = await delay1Second();
  1. ภายในdelay1Second()ฟังก์ชั่นส่งกลับสัญญาทันทีdelay(1000) [[PromiseStatus]]: 'pendingขอเรียกว่าdelayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. ฟังก์ชัน Async จะรวมค่าส่งคืนไว้ภายในPromise.resolve()( Source ) เนื่องจากdelay1Secondเป็นฟังก์ชัน async เราจึงมี:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise)ส่งคืนdelayPromiseโดยไม่ต้องทำอะไรเลยเพราะอินพุตเป็นสัญญาอยู่แล้ว (ดูMDN Promise.resolve ):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. awaitรอจนกว่าdelayPromiseจะมีการตัดสิน
  • IF delayPromiseเป็นจริงด้วย PromiseValue = 1:
const result = 1; 
  • ELSE delayPromiseถูกปฏิเสธ:
// jump to catch block if there is any

กลับมารอสัญญา

async function delay1Second() {
  return await delay(1000);
}
  1. โทรdelay1Second();
const result = await delay1Second();
  1. ภายในdelay1Second()ฟังก์ชั่นส่งกลับสัญญาทันทีdelay(1000) [[PromiseStatus]]: 'pendingขอเรียกว่าdelayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. การรอคอยในพื้นที่จะรอจนกว่าจะdelayPromiseได้รับการตัดสิน
  • กรณีที่ 1 : delayPromiseเติมเต็มด้วย 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; 
  • กรณีที่ 2 : delayPromiseถูกปฏิเสธ:
// 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;

อภิธานศัพท์:

  • Settle: Promise.[[PromiseStatus]]การเปลี่ยนแปลงจากpendingเป็นresolvedหรือrejected

0

ที่นี่ฉันปล่อยให้โค้ดบางอย่างเป็นประโยชน์สำหรับคุณ

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

ฟังก์ชั่น "x" เป็นฟังก์ชัน async มากกว่าที่มี fucn อื่น ๆ หากจะลบผลตอบแทนที่พิมพ์ "more code ... "

ตัวแปร x เป็นเพียงฟังก์ชันอะซิงโครนัสซึ่งจะมีฟังก์ชันอะซิงโครนัสอื่นในส่วนหลักของรหัสเราเรียกใช้การรอเพื่อเรียกฟังก์ชันของตัวแปร x เมื่อมันเสร็จสมบูรณ์ตามลำดับของรหัสซึ่งจะเป็นเรื่องปกติ สำหรับ "async / await" แต่ภายในฟังก์ชัน x มีฟังก์ชันอะซิงโครนัสอีกฟังก์ชันหนึ่งและจะส่งกลับคำสัญญาหรือส่งกลับ "สัญญา" ซึ่งจะอยู่ภายในฟังก์ชัน x โดยลืมรหัสหลักนั่นคือจะไม่พิมพ์ "console.log (" more code .. ") ในทางกลับกันถ้าเราใส่" await "มันจะรอทุกฟังก์ชั่นที่เสร็จสมบูรณ์และสุดท้ายก็เป็นไปตามลำดับปกติของรหัสหลัก

ด้านล่าง "console.log (" เสร็จ 1 "ลบ" return "คุณจะเห็นลักษณะการทำงาน


1
แม้ว่ารหัสนี้อาจช่วยแก้ปัญหาได้รวมถึงคำอธิบายว่าทำไมจึงแก้ปัญหานี้ได้จะช่วยปรับปรุงคุณภาพของโพสต์ของคุณได้อย่างแท้จริงและอาจส่งผลให้มีการโหวตเพิ่มขึ้น จำไว้ว่าคุณกำลังตอบคำถามสำหรับผู้อ่านในอนาคตไม่ใช่แค่คนที่ถามตอนนี้ โปรดแก้ไขคำตอบของคุณเพื่อเพิ่มคำอธิบายและระบุข้อ จำกัด และสมมติฐานที่ใช้
Brian

0

นี่คือตัวอย่าง typescript ที่คุณสามารถเรียกใช้และโน้มน้าวตัวเองว่าคุณต้องการคำว่า "กลับมารอ"

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.