การผูกมัด async สัญญาด้วย 1 แก้ไขและ 1 ปฏิเสธ?

ฉันมีฟังก์ชันที่ต้องทำอะไรบางอย่างแบบอะซิงก์ในไม่กี่ขั้นตอน ทุกขั้นตอนอาจล้มเหลวได้ อาจล้มเหลวก่อนขั้นตอนที่ 1 ดังนั้นคุณอาจทราบผลลัพธ์ทันทีหรือหลังจาก 1.5 วินาที เมื่อล้มเหลว จะต้องเรียกใช้การโทรกลับ ระบุ เมื่อ สำเร็จ (ฉันใช้ เมื่อ อย่างตั้งใจ เพราะไม่ใช่แค่ ถ้า: จังหวะเป็นสิ่งสำคัญ)

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

นี่คือสิ่งที่ฉันมีตอนนี้ แต่มันไร้สาระ:

function clickSpeed() {
    return new Promise(function(resolve, reject) {
        if ( test('one') ) {
            return setTimeout(function() {
                if ( test('two') ) {
                    return setTimeout(function() {
                        if ( test('three') ) {
                            return setTimeout(function() {
                                console.log('resolving...');
                                resolve();
                            }, 500);
                        }
                        console.log('rejecting...');
                        reject();
                    }, 500);
                }
                console.log('rejecting...');
                reject();
            }, 500);
        }
        console.log('rejecting...');
        reject();
    });
}

(test() สุ่มผ่านหรือไม่ผ่านขั้นตอน)

เล่นซอที่นี่: http://jsfiddle.net/rudiedirkx/zhdrjjx1/

ฉันเดาว่าวิธีแก้ปัญหาคือการโยงสัญญา ซึ่งจะแก้ไขหรือปฏิเสธทุกขั้นตอน .. ? อาจจะ. นั่นคือสิ่งที่? ฉันจะใช้สิ่งนั้นได้อย่างไร?

สิ่งนี้ใช้ได้กับขั้นตอนที่ไม่ทราบจำนวนหรือไม่


person Rudie    schedule 23.11.2015    source แหล่งที่มา
comment
มันจะล้มเหลวทันทีที่คุณปล่อยให้มันล้มเหลว ไม่แน่ใจว่าฉันเข้าใจว่าคำถามคืออะไร   -  person Bergi    schedule 23.11.2015
comment
ผลลัพธ์ที่คาดหวังของ return ที่ return setTimeout(function() { คืออะไร?   -  person guest271314    schedule 23.11.2015
comment
@ guest271314: ฉันคิดว่า return อยู่ที่นั่นเพียงเพื่อป้องกันไม่ให้ console.log('rejecting...'); reject(); ทำงาน   -  person Bergi    schedule 23.11.2015
comment
@Bergi ฉันจะปล่อยให้มันล้มเหลวเมื่อใด ขั้นตอนเป็นแบบอะซิงโครนัส เลยไม่รู้ว่าขั้นตอนต่อไปจะสำเร็จหรือไม่ ฉันต้องตรวจสอบทุกขั้นตอนอย่างชัดเจนเหมือนในโค้ดด้านบนหรือไม่   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 การกลับมาเป็นเพียงการทำลายกระแสดังนั้นจึงไม่ปฏิเสธ   -  person Rudie    schedule 23.11.2015
comment
ฉันคิดว่าคุณต้องการสิ่งนี้ : promise.then(function(res, reject) { res('step1') or reject('err')}).then(function(res) { res('step2')}).catch(function(err) { console.log(err) })   -  person Errorpro    schedule 23.11.2015
comment
@Rudie: ใช่ คุณต้องปล่อยให้มันล้มเหลวอย่างชัดเจนในทุกขั้นตอน เพราะถ้าคุณไม่ทำ มันก็จะไม่ล้มเหลว แน่นอนว่าคุณควรใช้คำสัญญาแตกต่างออกไปมาก   -  person Bergi    schedule 23.11.2015
comment
แต่เมื่อมันล้มเหลวเมื่อไหร่? ข้างใน/ก่อนก้าวใดๆ , การกลับเป็นเพียงการหยุดการไหลจึงไม่ปฏิเสธ test ไม่ปฏิเสธสัญญา ? reject จะถูกเรียกอย่างไรตามค่าส่งคืนของ test ถ้า setTimeout ทำลายโฟลว์   -  person guest271314    schedule 23.11.2015
comment
@Bergi แตกต่างอย่างไร? แน่นอนทำไม?   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 test() ส่งคืนบูลเท่านั้น อาจไม่ใช่ฟังก์ชัน แต่เป็นคำสั่ง หรือตรวจสอบวัตถุ/คุณสมบัติอื่น การปฏิเสธเกิดขึ้น หลัง ทุก ๆ IF   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 test() ส่งคืนบูล แต่การสุ่มมีไว้สำหรับการทดสอบเท่านั้น จะนำอะไรก็ตามจากที่อื่นซึ่งอาจปฏิเสธคำสัญญาหรือก้าวไปสู่ขั้นตอนต่อไป การตรวจสอบtest()ไม่ใช่ฟังก์ชันเสมอไปและไม่ทราบเกี่ยวกับคำสัญญา   -  person Rudie    schedule 23.11.2015


คำตอบ (3)


คุณสามารถเขียนวิธีแก้ปัญหาของคุณใหม่เพื่อให้สัญญาได้อย่างแท้จริง:

function sleep(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

function clickSpeed() {
    if ( test('one') ) {
        return sleep(500).then(function() {
            if ( test('two') ) {
                return sleep(500).then(function() {
                    if ( test('three') ) {
                        return sleep(500).then(function() {
                            console.log('resolving...');
                        });
                    }
                    console.log('rejecting...');
                    return Promise.reject();
                });
            }
            console.log('rejecting...');
            return Promise.reject();
        });
    }
    console.log('rejecting...');
    return Promise.reject();
}

อย่างไรก็ตาม นั่นยังค่อนข้างน่าเกลียดอยู่ ฉันอยากจะย้อนกลับ if/elses เป็นขั้นตอนแรก:

function clickSpeed() {
    if (!test('one')) {
        console.log('rejecting...');
        return Promise.reject();
    }
    return sleep(500).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            return Promise.reject();
        }
        return sleep(500).then(function() {
            if (!test('three')) {
                console.log('rejecting...');
                return Promise.reject();
            }
            return sleep(500).then(function() {
                console.log('resolving...');
            });
        });
    });
}

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

function clickSpeed() {
    return Promise.resolve() // only so that the callbacks look alike, and to use throw
    .then(function() {
        if (!test('one')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500)
    }).then(function() {
        if (!test('three')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        console.log('resolving...');
    });
}

ตอนนี้คุณสามารถทำให้ tests เหล่านั้นส่งข้อยกเว้นได้เอง ดังนั้นคุณไม่จำเป็นต้องใช้ if อีกต่อไป และคุณสามารถย้ายคำสั่ง console.log('rejecting...'); ใน .catch() ได้

และในขณะที่ฉันเดาว่านี่เป็นเพียงตัวอย่างของคุณที่ทุกขั้นตอนเหมือนกัน คุณสามารถสร้าง Promise Chain แบบไดนามิกได้อย่างง่ายดายจากรายการเช่นกัน:

function clickSpeed() {
    return ['one', 'two', 'three'].reduce(function(p, cur) {
        return p.then(function() {
            if (!test(cur))
                throw new Error('rejecting...');
            else
                return sleep(500);
        });
    }, Promise.resolve());
}
person Bergi    schedule 23.11.2015
comment
สามารถอธิบายผลกระทบของ throw ได้ที่โพสต์หรือไม่ - person guest271314; 23.11.2015
comment
นั่นคือสิ่งที่ฉันเล่นด้วย แต่ก็คิดไม่ออก: jsfiddle.net/rudiedirkx/ zhdrjjx1/1 ดูเหมือนมันมากกว่านี้! - person Rudie; 23.11.2015
comment
@ guest271314: throw; ก็ไม่ต่างจาก return Promise.reject(); เมื่ออยู่ในการโทรกลับ then - person Bergi; 23.11.2015
comment
@Rudie: อ่า ฉันพลาดที่จะดูซอของคุณ มันทนทุกข์ทรมานจาก Promise Constructor antipattern และไม่ทำงานเนื่องจากการเรียก reject() ไม่ทำลายเชนเหมือน ข้อยกเว้น - person Bergi; 23.11.2015
comment
@Bergi สิ่งที่ส่งคืนไปยัง onRejected จาก throw ? เกี่ยวกับ Promise Constructor antipattern stackoverflow.com/q/23803743/1048572 เป็นหลักฐานที่ new Promise(function(resolve, reject){}) ไม่ควรใช้หรือไม่ - person guest271314; 23.11.2015
comment
@ guest271314: ไม่มีอะไร นั่นคือ undefined เช่นเดียวกับอาร์กิวเมนต์ของ Promise.reject() - person Bergi; 23.11.2015
comment
@ guest271314: เกี่ยวกับรูปแบบการต่อต้านคอนสตรัคเตอร์ของ Promise โปรดอ่านคำตอบของคำถามที่เชื่อมโยง พวกเขาไม่ได้บอกว่าไม่เคย อันที่จริงฉันใช้มันอย่างถูกต้องในคำตอบนี้ (sleep) - person Bergi; 23.11.2015
comment
@Bergi ใช่และใช่อย่างแน่นอน ฉันรู้ว่ามีวิธีสัญญาที่ดี วิธีนี้สามารถเชื่อมโยงได้มากขึ้นด้วยฟังก์ชันที่เรียก clickSpeed() ซึ่งเป็นสิ่งที่ฉันหวังไว้ว่าจะเป็นไปได้ ขอบคุณ! - person Rudie; 23.11.2015
comment
@Bergi อ่านหน้า หลายต่อหลายครั้งแล้ว อะไรคือความแตกต่างระหว่าง sleep ตามที่โพสต์และ function sleep(ms) { Promise.resolve().then(function() { /*setTimeout here*/ })}? โดยเฉพาะอย่างยิ่ง @thefourtheyes คำตอบที่อ้างอิงถึง programmers.stackexchange.com/a/279003 ดูเหมือนจะสื่อถึงการไม่ใช้ Constructor เลยใช่หรือไม่ ขึ้นอยู่กับประสิทธิภาพ ? มีปัญหาที่นี่ในการพยายามตีความอย่างแน่นอน เมื่อใด เมื่อไม่ จะถามคำถามแม้ว่าอาจจะปิดเหมือนซ้ำกัน ไม่ชัดเจนทั้งหมด ที่นี่ จะประเมินอย่างไรเมื่อต้องใช้รูปแบบตัวสร้างหรือ Promise.resolve() ? - person guest271314; 23.11.2015
comment
@ guest271314: ใช้ Promise.resolve เมื่อทำได้ บางครั้งคุณไม่สามารถทำได้ คุณจะต้องใช้ตัวสร้าง Promise sleepเป็นเพียงตัวอย่างหนึ่ง การใช้งานของคุณไม่ได้ผล ลองดูว่าทำไม ไม่ สิ่งนี้ไม่เกี่ยวข้องกับประสิทธิภาพการทำงาน (ซึ่งจะเป็นเรื่องเฉพาะของไลบรารี โปรแกรมเมอร์คำถามที่คุณเชื่อมโยงดูเหมือนจะหารือเกี่ยวกับ Bluebird) - person Bergi; 23.11.2015

เนื่องจากจังหวะเวลาของคุณเป็นแบบโมโนโทน และ "งาน" ของคุณถูกกำหนดไว้ล่วงหน้าแล้ว ฉันจึงปรับโครงสร้างโค้ดใหม่เพื่อใช้ setInterval() พร้อมบริบทเพื่อติดตาม "งาน" หรือ "ขั้นตอน" ที่จำเป็น ในความเป็นจริง คุณ มี ที่จะใช้ Promise เมื่อทำเช่นนั้น แม้ว่าคุณจะยังสามารถทำได้ และหากคุณต้องการ chain handlers เพิ่มเติม เป็นความคิดที่ดี:

function clickSpeed(resolve, reject) {
  var interval = setInterval(function(work) {
    try {
      var current = work.shift();
      if(!test(current)) { // Do current step's "work"
        clearInterval(interval); // reject on failure and clear interval
        console.log('rejecting...', current);
        reject();
      }
      else if(!work.length) { // If this was the last step
        clearInterval(interval); // resolve (success!) and clear interval
        console.log('resolving...');
        resolve();
      }
    }
    catch(ex) { // reject on exceptions as well
      reject(ex);
      clearInterval(interval);
    }
  }, 500, ['one', 'two', 'three']); // "work" array
}

ฟังก์ชันนี้สามารถเรียกว่า "โดยตรง" ด้วยตัวจัดการการแก้ไข/ปฏิเสธ หรือใช้เป็นอาร์กิวเมนต์ของ Constructor ของ Promise

ดูตัวอย่างแบบเต็มใน JSFiddle ที่แก้ไขแล้ว


เพื่อจัดการกับความคิดเห็น สำเร็จรูปมากเกินไป ของ Bergi คุณสามารถเขียนโค้ดได้กระชับยิ่งขึ้นโดยไม่ต้องบันทึก:

function clickSpeed(resolve, reject) {
    function done(success, val) {
        clearInterval(interval);
        success ? resolve(val) : reject(val);
    }

    var interval = setInterval(function(work) {
        try {
            if(test(work.shift()) || done(false)) {
                work.length || done(true);
            }
        }
        catch(ex) { // reject on exceptions as well
            done(false, ex);
        }
    }, 500, ['one', 'two', 'three']); // "work" array
}
person Amit    schedule 26.11.2015
comment
เฮ้ มันฉลาดนัก! ฉันชอบมัน. - person Rudie; 26.11.2015
comment
ฉันขอแนะนำให้หลีกเลี่ยง setInterval เมื่อใช้คำสัญญา มันไม่เข้ากัน และคุณสูญเสียความปลอดภัยในการโยน และต้องกลับไปยุ่งกับการโทรกลับ นอกจากนี้ยังมีสำเร็จรูปมากเกินไป ... ความซ้ำซากจำเจสามารถนำไปใช้ในรหัสสัญญาได้อย่างง่ายดายเช่นกัน (ดูการอัปเดตคำตอบของฉัน) - person Bergi; 26.11.2015
comment
@Bergi - วิธีแก้ปัญหาไม่ได้ใช้สัญญาจริงๆ แต่เปิดใช้งานการห่อด้วย Promise การจัดการข้อยกเว้นถูกอบไว้ภายใน และกลไกการโทรกลับเป็นไปตามการออกแบบ (ตามที่ OP อธิบายไว้) Boilerplate มีเพียงเล็กน้อย (ดังที่แสดงในเวอร์ชันอัปเดต) และเทียบได้กับโซลูชันของคุณ - ซึ่งฉันคิดว่าก็ดีเช่นกัน - person Amit; 26.11.2015
comment
อ๊ะ ฉันคงอ่านคำสั่ง try catch นั้นมากเกินไป ฉันจะนำเรื่องนั้นกลับมา ฉันยังเดาว่าความจำเป็นสำหรับ try คือสิ่งที่ทำให้โซลูชันของคุณนานกว่าของฉันเล็กน้อย :-) - person Bergi; 26.11.2015

ขั้นแรก เราจะจัดเรียงคำกล่าวเปิดใหม่อย่างละเอียดเพื่อให้สะท้อนถึงการใช้คำสัญญาที่เป็นแบบฉบับและเหมาะสมยิ่งขึ้น

แทน :

ฟังก์ชั่นที่ต้องดำเนินการ บางอย่างแบบอะซิงก์ ในไม่กี่ขั้นตอน

สมมติว่า:

ฟังก์ชั่นที่ต้องทำบางอย่างในขั้นตอนอะซิงก์ไม่กี่ขั้นตอน

ดังนั้นก่อนอื่นเราอาจเลือกที่จะเขียนฟังก์ชันที่ดำเนินการตามสัญญาและส่งคืนผลลัพธ์ที่สัญญาไว้:

function doAnAsyncSequence() {
    return Promise.resolve()
    .then(function() {
        doSomethingAsync('one');
    })
    .then(function() {
        doSomethingAsync('two');
    })
    .then(function() {
        doSomethingAsync('three');
    });
}

และเพื่อการสาธิต เราสามารถเขียน doSomethingAsync() โดยให้ส่งคืนสัญญาที่มีโอกาส 50:50 ที่จะได้รับการแก้ไข:ถูกปฏิเสธ (ซึ่งมีประโยชน์มากกว่าที่นี่มากกว่าความล่าช้า):

function doSomethingAsync(x) {
    return new Promise(function(resolve, reject) {
        if(Math.random() > 0.5 ) {
            resolve(x);
        } else {
            reject(x); // importantly, this statement reports the input argument `x` as the reason for failure, which can be read and acted on where doSomethingAsync() is called.
        }
    });
}

จากนั้นคำถามหลักก็คือ:

เมื่อไหร่มันจะล้มเหลว?

อาจถูกเรียบเรียงใหม่:

มันล้มเหลวเมื่อไหร่?

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

ในกรณีของ doAnAsyncSequence() เราสามารถทำได้ดังนี้:

doAnAsyncSequence().then(function(result) {
    console.log(result); // if this line executes, it will always log "three", the result of the *last* step in the async sequence.
}, function(reason) {
    console.log('error: ' + reason);
});

แม้ว่าจะไม่มีคำสั่ง console.log() ใน doAnAsyncSequence() หรือ doSomethingAsync() :

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

ลองดู


นั่นคือทฤษฎี

เพื่อตอบคำถามเฉพาะเจาะจง (ตามที่ฉันเข้าใจ) ...

สำหรับ doSomethingAsync() เขียน:

function test_(value, delay) {
    return new Promise(function(resolve, reject) {
        //call your own test() function immediately
        var result = test(value);
        // resolve/reject the returned promise after a delay
        setTimeout(function() {
            result ? resolve() : reject(value);
        }, delay);
    });
}

สำหรับ doAnAsyncSequence() เขียน:

function clickSpeed() {
    var delayAfterTest = 500;
    return Promise.resolve()
    .then(function() {
        test_('one', delayAfterTest);
    })
    .then(function() {
        test_('two', delayAfterTest);
    })
    .then(function() {
        test_('three', delayAfterTest);
    });
}

และโทรดังนี้:

clickSpeed().then(function() {
    console.log('all tests passed');
}, function(reason) {
    console.log('test sequence failed at: ' + reason);
});
person Roamer-1888    schedule 26.11.2015
comment
อ่านได้ดีมาก แต่ฉันต้องการการหมดเวลา เนื่องจากสิ่งอื่นๆ ทำสิ่งที่ไม่พร้อมกันและฉันต้องรอ ~ 500ms - person Rudie; 26.11.2015
comment
อย่างที่ฉันพูดไป doSomethingAsync() ของฉันมีไว้เพื่อการสาธิต ในโค้ดโลกแห่งความเป็นจริงของคุณ คุณอาจเลือกที่จะเขียนสิ่งที่แตกต่างไปจากเดิมอย่างสิ้นเชิง - person Roamer-1888; 26.11.2015