จะรอให้ JavaScript Promise แก้ไขก่อนที่จะกลับมาทำงานได้อย่างไร


113

ฉันกำลังทำการทดสอบหน่วย กรอบการทดสอบจะโหลดเพจลงใน iFrame จากนั้นเรียกใช้การยืนยันกับเพจนั้น ก่อนการทดสอบแต่ละครั้งจะเริ่มขึ้นฉันจะสร้างสิ่งPromiseที่กำหนดให้onloadเหตุการณ์ของ iFrame เรียกresolve()ตั้งค่า iFrame srcและส่งคืนคำสัญญา

ดังนั้นฉันก็สามารถโทรloadUrl(url).then(myFunc)และมันจะรอให้การโหลดหน้าก่อนที่จะดำเนินสิ่งที่myFuncเป็น

ฉันใช้รูปแบบประเภทนี้ทั่วทุกที่ในการทดสอบของฉัน (ไม่ใช่แค่การโหลด URL) โดยหลักแล้วเพื่อให้การเปลี่ยนแปลง DOM เกิดขึ้น (เช่นเลียนแบบการคลิกปุ่มและรอให้ divs ซ่อนและแสดง)

ข้อเสียของการออกแบบนี้คือฉันมักจะเขียนฟังก์ชันที่ไม่ระบุตัวตนโดยมีโค้ดสองสามบรรทัดอยู่ในนั้น นอกจากนี้ในขณะที่ฉันมีวิธีแก้ไข (QUnit's assert.async()) ฟังก์ชันทดสอบที่กำหนดคำสัญญาจะเสร็จสมบูรณ์ก่อนที่สัญญาจะทำงาน

ฉันสงสัยว่ามีวิธีใดที่จะได้รับค่าจากPromiseหรือรอ (บล็อก / นอนหลับ) จนกว่าจะได้รับการแก้ไขที่คล้ายกับของ IAsyncResult.WaitHandle.WaitOne().NET ฉันรู้ว่า JavaScript เป็นเธรดเดียว แต่ฉันหวังว่านั่นไม่ได้หมายความว่าฟังก์ชันจะไม่สามารถให้ผลลัพธ์ได้

โดยพื้นฐานแล้วมีวิธีที่จะทำให้สิ่งต่อไปนี้คายผลลัพธ์ตามลำดับที่ถูกต้องหรือไม่?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>


หากคุณใส่การโทรต่อท้ายลงในฟังก์ชันที่ใช้ซ้ำได้คุณสามารถ () ได้ตามต้องการเพื่อ DRY คุณยังสามารถสร้างตัวจัดการเอนกประสงค์ซึ่งนำโดยสิ่งนี้เพื่อป้อนแล้ว () การโทรเช่น.then(fnAppend.bind(myDiv))ซึ่งสามารถลด anons ได้อย่างมาก
dandavis

คุณกำลังทดสอบกับอะไร? หากเป็นเบราว์เซอร์สมัยใหม่หรือคุณสามารถใช้เครื่องมือเช่น BabelJS เพื่อถ่ายทอดโค้ดของคุณสิ่งนี้เป็นไปได้อย่างแน่นอน
Benjamin Gruenbaum

คำตอบ:


81

ฉันสงสัยว่ามีวิธีใดบ้างในการรับค่าจาก Promise หรือรอ (block / sleep) จนกว่าจะได้รับการแก้ไขคล้ายกับ IAsyncResult ของ. NET.WaitHandle.WaitOne () ฉันรู้ว่า JavaScript เป็นเธรดเดียว แต่ฉันหวังว่านั่นไม่ได้หมายความว่าฟังก์ชันจะไม่สามารถให้ผลลัพธ์ได้

Javascript รุ่นปัจจุบันในเบราว์เซอร์ไม่มีwait()หรือsleep()ที่อนุญาตให้เรียกใช้สิ่งอื่น ๆ ดังนั้นคุณไม่สามารถทำสิ่งที่คุณขอได้ แต่มันมีการดำเนินการแบบ async ที่จะทำสิ่งนั้นแล้วโทรหาคุณเมื่อเสร็จสิ้น (ตามที่คุณใช้สัญญา)

ส่วนหนึ่งเป็นเพราะเธรดเดียวของ Javascript หากเธรดเดียวกำลังหมุนอยู่จะไม่มี Javascript อื่นใดที่สามารถดำเนินการได้จนกว่าเธรดการกรอนั้นจะเสร็จสิ้น ES6 แนะนำyieldและเครื่องกำเนิดไฟฟ้าซึ่งจะช่วยให้สามารถใช้เทคนิคการทำงานร่วมกันเช่นนั้นได้ แต่เราค่อนข้างมีวิธีในการใช้งานเหล่านี้ในเบราว์เซอร์ที่ติดตั้งจำนวนมาก (สามารถใช้ในการพัฒนาฝั่งเซิร์ฟเวอร์บางส่วนที่คุณควบคุมเครื่องยนต์ JS ที่กำลังใช้งาน)


การจัดการรหัสตามสัญญาอย่างรอบคอบสามารถควบคุมลำดับการดำเนินการสำหรับการดำเนินการ async จำนวนมากได้

ฉันไม่แน่ใจว่าฉันเข้าใจคำสั่งที่คุณพยายามจะบรรลุในรหัสของคุณ แต่คุณสามารถทำสิ่งนี้ได้โดยใช้kickOff()ฟังก์ชันที่มีอยู่ของคุณจากนั้นแนบ.then()ตัวจัดการเข้ากับมันหลังจากเรียกมัน:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");

    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

สิ่งนี้จะส่งคืนผลลัพธ์ตามลำดับที่รับประกัน - ดังนี้:

start
middle
end

อัปเดตในปี 2018 (สามปีหลังจากเขียนคำตอบนี้):

หากคุณเปลี่ยนรหัสของคุณหรือเรียกใช้รหัสของคุณในสภาพแวดล้อมที่รองรับคุณสมบัติ ES7 เช่นasyncและawaitตอนนี้คุณสามารถใช้awaitเพื่อทำให้รหัสของคุณ "ปรากฏ" เพื่อรอผลของคำสัญญา มันยังคงพัฒนาไปพร้อมกับคำสัญญา มันยังไม่บล็อก Javascript ทั้งหมด แต่จะช่วยให้คุณสามารถเขียนการดำเนินการตามลำดับในไวยากรณ์ที่เป็นมิตรได้

แทนที่จะใช้วิธี ES6 ในการทำสิ่งต่างๆ:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

คุณสามารถทำได้:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});

3
ถูกต้องการใช้then()คือสิ่งที่ฉันทำ ฉันไม่ชอบที่จะต้องเขียนfunction() { ... }ตลอดเวลา มันถ่วงรหัส
dx_over_dt

3
@dfoverdx - การเข้ารหัส async ใน Javascript เกี่ยวข้องกับการเรียกกลับเสมอดังนั้นคุณจึงต้องกำหนดฟังก์ชัน (ไม่ระบุชื่อหรือตั้งชื่อ) ไม่มีทางหลีกเลี่ยงได้ในขณะนี้
jfriend00

2
แม้ว่าคุณจะสามารถใช้สัญกรณ์ลูกศร ES6 เพื่อทำให้สั้นลงได้ (foo) => { ... }แทนfunction(foo) { ... }
Rob H

1
การเพิ่มความคิดเห็นใน @RobH บ่อยครั้งคุณสามารถเขียนfoo => single.expression(here)เพื่อกำจัดวงเล็บปีกกาและกลม
ต้น

3
@dfoverdx - สามปีหลังจากคำตอบของฉันฉันอัปเดตด้วย ES7 asyncและawaitไวยากรณ์เป็นตัวเลือกซึ่งตอนนี้มีให้บริการใน node.js และในเบราว์เซอร์สมัยใหม่หรือผ่านทรานส์ไพเลอร์
jfriend00

14

หากใช้ ES2016 คุณสามารถใช้asyncและawaitและทำสิ่งต่างๆเช่น:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

ถ้าใช้ ES2015 คุณสามารถใช้เครื่องปั่นไฟ ถ้าคุณไม่ชอบไวยากรณ์คุณสามารถนามธรรมมันออกไปโดยใช้asyncฟังก์ชั่นยูทิลิตี้เป็นอธิบายที่นี่

หากใช้ ES5 คุณอาจต้องการไลบรารีเช่นBluebirdเพื่อให้คุณควบคุมได้มากขึ้น

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


1
ตัวอย่างเครื่องกำเนิดไฟฟ้าของคุณไม่ทำงาน แต่ขาดนักวิ่ง โปรดอย่าแนะนำเครื่องกำเนิดไฟฟ้าอีกต่อไปเมื่อคุณสามารถถ่ายโอน ES8 async/ awaitได้
Bergi

3
ขอบคุณสำหรับคำติชม ฉันได้ลบตัวอย่าง Generator และเชื่อมโยงกับบทช่วยสอน Generator ที่ครอบคลุมมากขึ้นพร้อมไลบรารี OSS ที่เกี่ยวข้องแทน
Josh Habdas

12
นี่เป็นการปิดคำสัญญา ฟังก์ชัน async จะไม่รออะไรเลยเมื่อคุณเรียกใช้มันจะส่งคืนคำสัญญา async/ awaitเป็นเพียงไวยากรณ์อื่นสำหรับPromise.then.
rustyx

คุณกำลังอย่างถูกasyncจะไม่รอ ... จนกว่าจะมีอัตราผลตอบแทนawaitโดยใช้ ตัวอย่าง ES2016 / ES7 ยังไม่สามารถรัน SO ได้ดังนั้นนี่คือโค้ดบางส่วนที่ฉันเขียนเมื่อสามปีก่อนเพื่อแสดงพฤติกรรม
Josh Habdas

8

อีกทางเลือกหนึ่งคือใช้ Promise.all เพื่อรอคำสัญญามากมายที่จะแก้ไขจากนั้นจึงดำเนินการกับสิ่งเหล่านั้น

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

function append_output(suffix, value) {
  $("#output_"+suffix).append(value)
}

function kickOff() {
  let start = new Promise((resolve, reject) => {
    append_output("now", "start")
    resolve("start")
  })
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => {
      append_output("now", " middle")
      resolve(" middle")
    }, 1000)
  })
  let end = new Promise((resolve, reject) => {
    append_output("now", " end")
    resolve(" end")
  })

  Promise.all([start, middle, end]).then(results => {
    results.forEach(
      result => append_output("later", result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Updated during execution: <div id="output_now"></div>
Updated after all have completed: <div id="output_later"></div>


วิธีนี้ใช้ไม่ได้เช่นกัน สิ่งนี้ทำให้สัญญาดำเนินไปตามลำดับเท่านั้น อะไรก็ตามหลังจาก kickOff () จะทำงานก่อนที่สัญญาจะเสร็จสิ้น
Philip Rego

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

BTW: คุณสามารถคีบซอนี้และเล่นด้วยรหัส: jsfiddle.net/akasek/4uxnkrez/12
Stan Kurdziel

-1

คุณสามารถทำได้ด้วยตนเอง (ฉันรู้ว่านั่นไม่ใช่วิธีแก้ปัญหาที่ดี แต่ .. ) ใช้whileลูปจนกว่าresultจะไม่มีค่า

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});

2
สิ่งนี้จะใช้ CPU ทั้งหมดในขณะที่รอ
Lukasz Guminski

นี่เป็นวิธีแก้ปัญหาที่น่ากลัว
JSON

ใช่ แต่นี่เป็นวิธีเดียวที่จะได้ผลโดยไม่ต้องรังฟังก์ชันหรือสัญญา แม้กระทั่งบรรทัดหลังจากฟังก์ชัน / สัญญาที่ซ้อนกันจะทำงานก่อนที่จะเสร็จสมบูรณ์
Philip Rego

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