Javascript สัญญากับความอยากรู้อยากเห็น


96

เมื่อฉันเรียกสัญญานี้ผลลัพธ์จะไม่ตรงกับลำดับของการเรียกฟังก์ชัน การ.thenมาก่อน.catchแม้ว่าสัญญากับ.thenจะถูกเรียกตาม เหตุผลนั้นคืออะไร?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

เอาท์พุท

node promises.js
response: true
error: false

34
คุณไม่ควรพึ่งพาการกำหนดเวลาระหว่างกลุ่มคำสัญญาที่เป็นอิสระ
Bergi

คำตอบ:


136

นี่เป็นคำถามที่น่าสนใจที่จะอ่านด้านล่าง

เมื่อคุณทำสิ่งนี้:

verifier(3,4).then(...)

ที่ส่งคืนสัญญาใหม่ซึ่งต้องใช้วงจรอื่นกลับไปที่ลูปเหตุการณ์ก่อนที่สัญญาที่เพิ่งถูกปฏิเสธจะสามารถรัน.catch()ตัวจัดการที่ตามมาได้ รอบพิเศษนั้นให้ลำดับถัดไป:

verifier(5,4).then(...)

มีโอกาสเรียกใช้.then()ตัวจัดการก่อนบรรทัดก่อนหน้า.catch()เนื่องจากมีอยู่ในคิวก่อนที่.catch()ตัวจัดการจากตัวแรกจะเข้ามาในคิวและรายการจะถูกเรียกใช้จากคิวในลำดับ FIFO


โปรดทราบว่าหากคุณใช้.then(f1, f2)แบบฟอร์มแทนฟอร์ม.then().catch()จะทำงานเมื่อคุณคาดว่าจะเป็นเพราะไม่มีสัญญาเพิ่มเติมจึงไม่มีเห็บเพิ่มเติมที่เกี่ยวข้อง:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

หมายเหตุฉันยังติดป้ายกำกับข้อความทั้งหมดเพื่อให้คุณสามารถดูได้verifier()ว่าพวกเขามาจากสายไหนซึ่งทำให้อ่านผลลัพธ์ได้ง่ายขึ้นมาก


ข้อมูลจำเพาะ ES6 เกี่ยวกับการสั่งซื้อการโทรกลับตามสัญญาและคำอธิบายโดยละเอียดเพิ่มเติม

ข้อมูลจำเพาะ ES6 บอกเราว่าสัญญาว่า "งาน" (ตามที่เรียกการเรียกกลับจาก a .then()หรือ.catch()) จะทำงานในลำดับ FIFO ตามเวลาที่แทรกลงในคิวงาน ไม่ได้ระบุชื่อ FIFO โดยเฉพาะ แต่ระบุว่ามีการแทรกงานใหม่ที่ส่วนท้ายของคิวและงานจะถูกรันจากจุดเริ่มต้นของคิว ที่ดำเนินการสั่งซื้อ FIFO

PerformPromiseThen (ซึ่งเรียกใช้การเรียกกลับจาก.then()) จะนำไปสู่EnqueueJobซึ่งเป็นวิธีที่ตัวจัดการการแก้ไขหรือปฏิเสธได้รับการกำหนดให้ทำงานจริง EnqueueJob ระบุว่างานที่รอดำเนินการถูกเพิ่มที่ด้านหลังของคิวงาน จากนั้นการดำเนินการNextJobจะดึงรายการจากด้านหน้าของคิว สิ่งนี้ช่วยให้มั่นใจได้ถึงคำสั่ง FIFO ในงานบริการจากคิวงาน Promise

ดังนั้นในตัวอย่างในคำถามเดิมเราได้รับการเรียกกลับสำหรับverifier(3,4)คำสัญญาและverifier(5,4)คำสัญญาที่แทรกลงในคิวงานตามลำดับที่รันเนื่องจากสัญญาเดิมทั้งสองเสร็จสิ้น จากนั้นเมื่อล่ามกลับไปที่ลูปเหตุการณ์ก็จะรับverifier(3,4)งานก่อน คำสัญญานั้นถูกปฏิเสธและไม่มีการติดต่อกลับสำหรับสิ่งนั้นในverifier(3,4).then(...). ดังนั้นสิ่งที่ทำคือปฏิเสธสัญญาที่verifier(3,4).then(...)ส่งคืนและนั่นทำให้verifier(3,4).then(...).catch(...)ตัวจัดการถูกแทรกลงใน jobQueue

จากนั้นจะกลับไปที่ลูปเหตุการณ์และงานถัดไปที่ดึงมาจาก jobQueue คือverifier(5, 4)งาน นั่นมีสัญญาที่ได้รับการแก้ไขและตัวจัดการการแก้ไขจึงเรียกตัวจัดการนั้น สิ่งนี้ทำให้response (5,4):เอาต์พุตถูกแสดง

จากนั้นมันจะกลับไปที่ลูปเหตุการณ์และงานถัดไปที่ดึงมาจาก jobQueue คือverifier(3,4).then(...).catch(...)งานที่รันมันและทำให้error (3,4)เอาต์พุตถูกแสดง

เป็นเพราะ.catch()ในห่วงโซ่ที่ 1 เป็นระดับสัญญาหนึ่งที่อยู่ลึกลงไปในห่วงโซ่มากกว่า.then()ในห่วงโซ่ที่ 2 ซึ่งทำให้เกิดการสั่งซื้อที่คุณรายงาน และเป็นเพราะโซ่สัญญาข้ามจากระดับหนึ่งไปอีกระดับผ่านคิวงานในลำดับ FIFO ไม่ใช่ซิงโครนัส


คำแนะนำทั่วไปเกี่ยวกับการใช้รายละเอียดการจัดกำหนดการระดับนี้

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


เพื่อให้เจาะจงมากขึ้นพฤติกรรมที่แน่นอนนี้ไม่ได้รับการบันทึกไว้ที่ใดก็ได้ในข้อกำหนดของสัญญาซึ่งทำให้เป็นรายละเอียดการดำเนินการ คุณอาจมีพฤติกรรมที่แตกต่างกันระหว่างล่าม (เช่น Node.js กับ Edge vs Firefox) หรือระหว่างเวอร์ชันของล่าม (เช่นโหนด 12 กับโหนด 14) ข้อมูลจำเพาะบอกเพียงว่าสัญญาถูกประมวลผลแบบอะซิงโครนัสเพื่อหลีกเลี่ยงรหัส zalgo (ซึ่ง IMHO เข้าใจ BTW ผิดเพราะได้รับแรงบันดาลใจจากผู้ที่ถามคำถามเช่นนี้โดยต้องการขึ้นอยู่กับเวลาของรหัสอะซิงโครนัสที่อาจเกิดขึ้น)
slebetman

@slebetman - ไม่ได้มีการบันทึกไว้ว่าการเรียกกลับจากสัญญาที่แยกจากกันเรียกว่า FIFO ตามเวลาที่แทรกลงในคิวและไม่สามารถทำงานได้จนกว่าจะถึงขีดถัดไป? ดูเหมือนว่าการสั่งซื้อ FIFO เป็นสิ่งที่จำเป็นทั้งหมดที่นี่เนื่องจาก.then()ต้องส่งคืนสัญญาใหม่ที่ตัวเองต้องแก้ไข / ปฏิเสธแบบอะซิงโครนัสในเครื่องหมายในอนาคตซึ่งเป็นสิ่งที่นำไปสู่การสั่งซื้อนี้ คุณรู้หรือไม่ว่าการนำไปใช้งานใด ๆ ที่ไม่ใช้ลำดับ FIFO ของการเรียกกลับที่แข่งขันกัน
jfriend00

3
@slebetman Promises / A + ไม่ได้ระบุไว้ ES6 ระบุไว้ (ES11 เปลี่ยนพฤติกรรมของawaitแม้ว่า)
Bergi

จากข้อมูลจำเพาะ ES6 ในลำดับการเข้าคิว PerformPromiseThenจะนำไปสู่EnqueueJobวิธีที่ตัวจัดการการแก้ไขหรือปฏิเสธได้รับการกำหนดเวลาให้เรียก EnqueueJob ระบุว่างานที่รอดำเนินการถูกเพิ่มที่ด้านหลังของคิวงาน จากนั้นการดำเนินการNextJobจะดึงรายการจากด้านหน้าของคิว สิ่งนี้ทำให้มั่นใจได้ถึงคำสั่ง FIFO ในคิวงาน Promise
jfriend00

@Bergi การเปลี่ยนแปลงนี้awaitใน ES11 คืออะไร? ลิงค์พอเพียง ขอบคุณ !!
Pedro A

49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

แทนที่จะเป็นเอาต์พุต a1, a2, a3, b1, b2, b3 คุณจะเห็น a1, b1, a2, b2, a3, b3 เนื่องจากเหตุผลเดียวกัน - ทุกครั้งจะส่งคืนสัญญาและไปที่จุดสิ้นสุดของ event-loop คิว. เราจะเห็น "การแข่งขันแห่งสัญญา" นี้ เช่นเดียวกับเมื่อมีสัญญาบางอย่างซ้อนกัน

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