ฉันจะเข้าถึงผลลัพธ์ของสัญญาก่อนหน้านี้ในห่วงโซ่แล้ว. () ได้อย่างไร


650

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

function getExample() {
    return promiseA(…).then(function(resultA) {
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        return // How do I gain access to resultA here?
    });
}

2
คำถามนี้น่าสนใจจริงๆและแม้ว่าจะมีการติดแท็กjavascriptแต่ก็มีความเกี่ยวข้องในภาษาอื่น ฉันใช้คำตอบ "break the chain"ใน java และjdeferred
gontard

คำตอบ:


377

ทำลายโซ่

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

สิ่งนี้จะส่งผลให้การควบคุมการไหลตรงไปตรงมาอย่างชัดเจนองค์ประกอบที่ชัดเจนของฟังก์ชันการใช้งาน

function getExample() {
    var a = promiseA(…);
    var b = a.then(function(resultA) {
        // some processing
        return promiseB(…);
    });
    return Promise.all([a, b]).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

แทนการ destructuring พารามิเตอร์ในการเรียกกลับหลังจากPromise.allที่กลายเป็นเพียงใช้ได้กับ ES6 ใน ES5 thenโทรจะถูกแทนที่ด้วยวิธีการช่วยเหลือที่ดีที่ได้รับจากห้องสมุดสัญญาจำนวนมาก ( Q , คราม , เมื่อ , ... .spread(function(resultA, resultB) { …):

Bluebird ยังมีjoinฟังก์ชั่นเฉพาะเพื่อแทนที่Promise.all+ spreadชุดที่มีโครงสร้างที่เรียบง่าย (และมีประสิทธิภาพมากขึ้น):


return Promise.join(a, b, function(resultA, resultB) {  });

1
ฟังก์ชั่นภายในอาเรย์ดำเนินการตามลำดับหรือไม่
scaryguy

6
@scaryguy: ไม่มีฟังก์ชั่นในอาเรย์ promiseAและpromiseBเป็นฟังก์ชั่น (สัญญาคืน) ที่นี่
Bergi

2
@Roland ไม่เคยบอกว่ามันเป็น :-) คำตอบนี้ถูกเขียนขึ้นในยุค ES5 ที่ไม่มีสัญญาใดที่ได้มาตรฐานเลยและspreadมันมีประโยชน์มากในรูปแบบนี้ สำหรับคำตอบที่ทันสมัยยิ่งขึ้นดูคำตอบที่ยอมรับได้ อย่างไรก็ตามฉันได้อัปเดตคำตอบอย่างชัดเจนแล้วและไม่มีเหตุผลที่ดีที่จะไม่อัปเดตคำตอบนี้เช่นกัน
Bergi

1
@ แก้ไขไม่คุณไม่ควรทำอย่างนั้นมันจะทำให้เกิดปัญหากับการปฏิเสธ
Bergi

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

238

ECMAScript Harmony

แน่นอนว่าปัญหานี้ได้รับการยอมรับจากนักออกแบบภาษาด้วยเช่นกัน พวกเขาทำงานหนักมากและข้อเสนอฟังก์ชั่น asyncก็ทำให้เป็นจริง

ECMAScript 8

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

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

ในขณะที่เรากำลังรอ ES8 เราได้ใช้ไวยากรณ์ที่คล้ายกันมากแล้ว ES6 มาพร้อมกับฟังก์ชั่นเครื่องกำเนิดซึ่งอนุญาตให้แยกการทำงานออกเป็นชิ้น ๆ โดยใช้yieldคำหลักที่วางไว้โดยพล ชิ้นส่วนเหล่านั้นสามารถทำงานต่อกันได้อย่างอิสระแม้แต่แบบอะซิงโครนัส - และนั่นเป็นสิ่งที่เราทำเมื่อเราต้องการรอการแก้ไขสัญญาก่อนที่จะดำเนินการขั้นตอนต่อไป

มีไลบรารีเฉพาะ (เช่นcoหรือtask.js ) แต่ยังมีห้องสมุดสัญญาอีกหลายแห่งที่มีฟังก์ชั่นผู้ช่วย ( Q , Bluebird , เมื่อ , ... ) ที่ทำสิ่งนี้ async การดำเนินการทีละขั้นตอนสำหรับคุณเมื่อคุณให้ฟังก์ชั่นเครื่องกำเนิดไฟฟ้า สัญญาผลผลิต

var getExample = Promise.coroutine(function* () {
//               ^^^^^^^^^^^^^^^^^ Bluebird syntax
    var resultA = yield promiseA(…);
    // some processing
    var resultB = yield promiseB(…);
    // more processing
    return // something using both resultA and resultB
});

สิ่งนี้ทำงานได้ใน Node.js ตั้งแต่เวอร์ชัน 4.0 และมีเบราว์เซอร์บางตัว (หรือรุ่น dev) ของพวกเขาได้สนับสนุนไวยากรณ์ของตัวสร้างค่อนข้างเร็ว

ECMAScript 5

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

จากนั้นยังมีภาษาอื่น ๆ ที่รวบรวมเพื่อ JS ที่ทุ่มเทให้กับการทำให้การเขียนโปรแกรมแบบอะซิงโครนัสผ่อนคลายลง พวกเขามักจะใช้ไวยากรณ์ที่คล้ายกับawait(เช่นIced CoffeeScript ) แต่ก็มีคนอื่น ๆ ที่มีคุณลักษณะการอธิบายเหมือนdoHaskell (เช่นLatteJs , monadic , PureScriptหรือLispyScript )


@Bergi คุณต้องรอฟังก์ชั่น async สอบ getExample () จากรหัสภายนอกหรือไม่
arisalexis

@arisalexis: ใช่getExampleยังคงเป็นฟังก์ชั่นที่คืนสัญญาให้ทำงานเหมือนกับฟังก์ชั่นในคำตอบอื่น ๆ แต่มีไวยากรณ์ที่ดีกว่า คุณสามารถawaitโทรในasyncฟังก์ชั่นอื่นหรือคุณสามารถเชื่อมโยง.then()กับผลของมัน
Bergi

1
ฉันอยากรู้ว่าทำไมคุณตอบคำถามของคุณเองทันทีหลังจากถาม มีการสนทนาที่ดีที่นี่ แต่ฉันอยากรู้ คุณอาจพบคำตอบด้วยตัวเองหลังจากถาม
Granmoe

@granmoe: ผมโพสต์การสนทนาทั้งหมดเกี่ยวกับวัตถุประสงค์เป็นเป้าหมายที่ซ้ำกันที่ยอมรับ
Bergi

มีวิธี (ไม่ลำบากเกินไป) ในการหลีกเลี่ยงการใช้ Promise.coroutine (เช่นไม่ใช้ Bluebird หรือไลบรารีอื่น แต่เป็นเพียง JS ธรรมดา) ในตัวอย่าง ECMAScript 6 ที่มีฟังก์ชันตัวสร้าง? ฉันมีบางสิ่งบางอย่างในใจsteps.next().value.then(steps.next)...แต่ไม่ได้ผล
ผู้เรียนไม่มีชื่อ

102

การตรวจสอบแบบซิงโครนัส

การกำหนดสัญญาที่จำเป็นสำหรับค่าในภายหลังให้กับตัวแปรจากนั้นรับค่าของพวกเขาผ่านการตรวจสอบแบบซิงโครนัส ตัวอย่างใช้.value()วิธีการของ Bluebird แต่มีหลายไลบรารีที่ให้วิธีการที่คล้ายกัน

function getExample() {
    var a = promiseA(…);

    return a.then(function() {
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // a is guaranteed to be fulfilled here so we can just retrieve its
        // value synchronously
        var aValue = a.value();
    });
}

สามารถใช้เป็นค่าได้มากเท่าที่คุณต้องการ:

function getExample() {
    var a = promiseA(…);

    var b = a.then(function() {
        return promiseB(…)
    });

    var c = b.then(function() {
        return promiseC(…);
    });

    var d = c.then(function() {
        return promiseD(…);
    });

    return d.then(function() {
        return a.value() + b.value() + c.value() + d.value();
    });
}

6
นี่คือคำตอบที่ฉันโปรดปราน: สามารถอ่านขยายได้และพึ่งพาฟีเจอร์ไลบรารีหรือภาษาน้อยที่สุด
Jason

13
@ Jason: เอ่อ " ความเชื่อมั่นในฟีเจอร์ของห้องสมุด " น้อยที่สุด ? การตรวจสอบแบบซิงโครนัสเป็นคุณลักษณะของไลบรารีและเป็นสิ่งที่ไม่ได้มาตรฐานในการบูต
Bergi

2
ฉันคิดว่าเขาหมายถึงคุณสมบัติเฉพาะของห้องสมุด
Deathgaze

54

การทำรัง (และ) ปิด

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

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

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

function getExample() {
    // preprocessing
    return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
    return function(resultA) {
        // some processing
        return promiseB(…).then(makeBhandler(resultA, …));
    };
}
function makeBhandler(resultA, …) {
    return function(resultB) {
        // more processing
        return // anything that uses the variables in scope
    };
}

นอกจากนี้คุณยังสามารถใช้ฟังก์ชั่นช่วยสำหรับชนิดของแอพลิเคชันบางส่วนเช่น_.partialจากเน้น / lodashหรือพื้นเมือง.bind()วิธีการเพื่อส่งเสริมการลดการเยื้อง:

function getExample() {
    // preprocessing
    return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
    // some processing
    return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
    // more processing
    return // anything that uses resultA and resultB
}

5
ข้อเสนอแนะเดียวกันนี้จะได้รับเป็นวิธีการขั้นสูงผิดพลาด # 4 'ในบทความโนแลนลอว์สันในสัญญาpouchdb.com/2015/05/18/we-have-a-problem-with-promises.html มันเป็นการอ่านที่ดี
Robert

2
นี่คือbindฟังก์ชั่นใน Monads Haskell นำเสนอ syntactic sugar (do-notation) เพื่อให้ดูเหมือน async / await syntax
zeronone

50

การส่งผ่านอย่างชัดเจน

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

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

ที่นี่ลูกศรเล็ก ๆb => [resultA, b]นั้นเป็นฟังก์ชั่นที่ปิดresultAและส่งผ่านอาร์เรย์ของผลลัพธ์ทั้งสองไปยังขั้นตอนถัดไป ซึ่งใช้ไวยากรณ์การทำลายโครงสร้างพารามิเตอร์เพื่อแยกค่าในตัวแปรเดี่ยวอีกครั้ง

ก่อนที่จะมีการทำลายโครงสร้างพร้อมใช้งานกับ ES6 วิธีการช่วยเหลือที่ดีที่.spread()ถูกเรียกใช้นั้นจัดทำโดยห้องสมุดสัญญาหลายแห่ง ( Q , Bluebird , เมื่อ , ... ) มันต้องใช้ฟังก์ชั่นที่มีหลายพารามิเตอร์ - หนึ่งสำหรับแต่ละองค์ประกอบอาร์เรย์ - .spread(function(resultA, resultB) { …เพื่อนำมาใช้เป็น

แน่นอนว่าการปิดที่จำเป็นในที่นี้สามารถทำให้ง่ายขึ้นโดยฟังก์ชันผู้ช่วยบางอย่างเช่น

function addTo(x) {
    // imagine complex `arguments` fiddling or anything that helps usability
    // but you get the idea with this simple one:
    return res => [x, res];
}


return promiseB(…).then(addTo(resultA));

หรือคุณสามารถใช้Promise.allเพื่อสร้างคำสัญญาสำหรับอาร์เรย์:

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
                                                    // as if passed to Promise.resolve()
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

และคุณอาจไม่เพียง แต่ใช้อาร์เรย์ แต่วัตถุที่ซับซ้อนโดยพลการ ตัวอย่างเช่นด้วย_.extendหรือObject.assignในฟังก์ชั่นตัวช่วยที่แตกต่างกัน:

function augment(obj, name) {
    return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(augment({resultA}, "resultB"));
    }).then(function(obj) {
        // more processing
        return // something using both obj.resultA and obj.resultB
    });
}

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


ครั้งแรกฉันไม่คิดว่าPromise.allควรละเว้นไวยากรณ์ที่ควรได้รับการสนับสนุน (จะไม่ทำงานใน ES6 เมื่อการทำลายล้างจะแทนที่มันและการเปลี่ยน.spreadเป็นการthenให้ผลที่ไม่คาดคิดแก่ผู้คนบ่อยครั้ง ณ วันที่เพิ่ม - ฉันไม่แน่ใจว่าทำไมคุณต้องการ การใช้ augment - การเพิ่มสิ่งต่าง ๆ ในต้นแบบสัญญาไม่ใช่วิธีที่ยอมรับได้ในการขยายสัญญา ES6 ต่อไปซึ่งควรจะขยายด้วย subclassing (ที่ไม่สนับสนุนในปัจจุบัน)
Benjamin Gruenbaum

@BenjaminGruenbaum: คุณหมายถึงอะไรโดย " ละเว้นไวยากรณ์Promise.all " ไม่มีวิธีการใดในคำตอบนี้ที่จะหยุดกับ ES6 การสลับ a spreadไปเป็น destructuring thenไม่ควรมีปัญหาเช่นกัน Re .prototype.augment: ฉันรู้ว่ามีคนสังเกตเห็นฉันแค่ชอบที่จะสำรวจความเป็นไปได้ - จะแก้ไขมัน
Bergi

โดยไวยากรณ์อาร์เรย์ฉันหมายถึงreturn [x,y]; }).spread(...แทนreturn Promise.all([x, y]); }).spread(...ซึ่งจะไม่เปลี่ยนแปลงเมื่อการแลกเปลี่ยนการแพร่กระจายสำหรับน้ำตาลทำลายโครงสร้าง es6 และจะไม่เป็นกรณีที่แปลกขอบที่สัญญารักษากลับอาร์เรย์แตกต่างจากทุกอย่างอื่น
Benjamin Gruenbaum

3
นี่อาจเป็นคำตอบที่ดีที่สุด สัญญาคือ "ฟังก์ชั่นการเขียนโปรแกรมปฏิกิริยา" - แสงและนี่มักจะเป็นวิธีการแก้ปัญหา ตัวอย่างเช่น BaconJs มี #combineTemplate ที่ช่วยให้คุณสามารถรวมผลลัพธ์เป็นวัตถุที่ผ่านโซ่
U Avalos

1
@CapiEtheriel คำตอบนั้นถูกเขียนขึ้นเมื่อ ES6 ไม่ได้เป็นวงกว้างเหมือนทุกวันนี้ ใช่อาจถึงเวลาเปลี่ยนตัวอย่าง
Bergi

35

สถานะบริบทที่ไม่แน่นอน

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

function getExample() {
    var resultA;
    return promiseA(…).then(function(_resultA) {
        resultA = _resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both resultA and resultB
    });
}

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

วิธีนี้มีข้อบกพร่องหลายประการ:

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

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

function getExample() {
    return promiseA(…)
    .bind({}) // Bluebird only!
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }).bind(); // don't forget to unbind the object if you don't want the
               // caller to access it
}

วิธีนี้สามารถจำลองได้ง่ายในห้องสมุดสัญญาที่ไม่สนับสนุน. ผูก (แม้ว่าในทางที่ค่อนข้างละเอียดและไม่สามารถใช้ในการแสดงออก):

function getExample() {
    var ctx = {};
    return promiseA(…)
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }.bind(ctx)).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }.bind(ctx));
}

.bind()ไม่จำเป็นสำหรับการป้องกันการรั่วไหลของหน่วยความจำ
Esailija

@Esailija: แต่สัญญาที่ส่งคืนไม่ได้มีการอ้างอิงถึงวัตถุบริบทเป็นอย่างอื่น? ตกลงแน่นอนการรวบรวมขยะจะจัดการในภายหลัง มันไม่ใช่ "รั่วไหล" เว้นแต่ว่าสัญญาจะไม่ถูกกำจัด
Bergi

ใช่ แต่สัญญายังถืออ้างอิงถึงการปฏิบัติตามค่านิยมของพวกเขาและเหตุผลข้อผิดพลาด ... แต่ไม่มีอะไรถืออ้างอิงถึงสัญญาจึงไม่ได้เรื่อง
Esailija

4
โปรดแบ่งคำตอบนี้ออกเป็นสองข้อเนื่องจากฉันเกือบจะลงคะแนนในคำนำ! ฉันคิดว่า "วิธีแก้ปัญหาเล็กน้อย (แต่ไม่ล้ำสมัยและค่อนข้างผิดพลาด)" เป็นวิธีที่สะอาดและง่ายที่สุดเนื่องจากไม่ต้องพึ่งพาการปิดและสถานะที่ไม่แน่นอนมากกว่าคำตอบที่คุณยอมรับเอง แต่ก็ง่ายกว่า การปิดไม่เป็นสากลหรือชั่วร้าย ข้อโต้แย้งที่ให้กับวิธีการนี้ไม่สมเหตุสมผลสำหรับฉันที่ได้รับหลักฐาน ปัญหาการทำให้เป็นโมดูล (modularization) ใดบ้างที่จะได้รับ
jib

2
ดังที่ฉันได้กล่าวไว้ข้างต้นคำสัญญาคือ "ฟังก์ชั่นการตอบโต้การเขียนโปรแกรม" - แสง นี่เป็นรูปแบบการต่อต้านใน FRP
U Avalos

15

สปินที่รุนแรงน้อยลงใน "สถานะบริบทที่เปลี่ยนแปลงไม่ได้"

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

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(paramsA).then(function(resultA){
        results.a = resultA;
        return promiseB(paramsB);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(paramsC);
    }).then(function(resultC){
        //Resolve with composite of all promises
        return Promise.resolve(results.a + results.b + resultC);
    }).catch(function(error){
        return Promise.reject(error);
    });
}
  • ตัวแปรโกลบอลไม่ดีดังนั้นโซลูชันนี้ใช้ตัวแปรที่กำหนดขอบเขตแบบโลคัลซึ่งไม่ทำให้เกิดอันตราย มันสามารถเข้าถึงได้เฉพาะภายในฟังก์ชั่น
  • สถานะที่ไม่แน่นอนเป็นสิ่งที่น่าเกลียด แต่ไม่ได้ทำให้เกิดการเปลี่ยนแปลงสถานะในลักษณะที่น่าเกลียด รัฐที่ไม่แน่นอนที่ไม่แน่นอนน่าจะหมายถึงการปรับเปลี่ยนสถานะของฟังก์ชั่นการโต้แย้งหรือตัวแปรทั่วโลก แต่วิธีการนี้จะปรับเปลี่ยนสถานะของตัวแปรที่กำหนดขอบเขตเฉพาะที่มีอยู่เพื่อจุดประสงค์เดียวของการรวมผลลัพธ์สัญญา ... ตัวแปรที่จะตายง่าย ๆ เมื่อสัญญาได้รับการแก้ไข
  • สัญญาระดับกลางจะไม่ถูกป้องกันไม่ให้เข้าถึงสถานะของวัตถุผลลัพธ์ แต่สิ่งนี้ไม่ได้แนะนำสถานการณ์ที่น่ากลัวซึ่งสัญญาข้อหนึ่งในห่วงโซ่จะหลอกลวงและก่อวินาศกรรมผลลัพธ์ของคุณ ความรับผิดชอบในการตั้งค่าในแต่ละขั้นตอนของสัญญาถูก จำกัด ไว้ที่ฟังก์ชั่นนี้และผลลัพธ์โดยรวมจะถูกต้องหรือไม่ถูกต้อง ... มันจะไม่เป็นข้อผิดพลาดบางอย่างที่จะครอบตัดในปีถัด ๆ ไปในการผลิต !)
  • สิ่งนี้ไม่แนะนำสถานการณ์การแย่งชิงที่อาจเกิดขึ้นจากการเรียกใช้แบบขนานเนื่องจากอินสแตนซ์ใหม่ของตัวแปรผลลัพธ์ถูกสร้างขึ้นสำหรับการเรียกใช้ฟังก์ชัน getExample ทุกครั้ง

1
อย่างน้อยก็หลีกเลี่ยงตัวPromiseสร้าง antipattern !
Bergi

ขอบคุณ @Bergi ฉันไม่รู้ด้วยซ้ำว่าเป็นการต่อต้านแบบจนกว่าคุณจะพูดถึงมัน!
Jay

นี่เป็นวิธีแก้ปัญหาที่ดีในการลดข้อผิดพลาดเกี่ยวกับสัญญาฉันใช้ ES5 และไม่ต้องการเพิ่มห้องสมุดอื่นเพื่อทำงานกับสัญญา
nilakantha singh deo

8

ตอนนี้โหนด 7.4 สนับสนุนการโทรแบบอะซิงก์ / รอสายพร้อมธงความสามัคคี

ลองสิ่งนี้:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

และเรียกใช้ไฟล์ด้วย:

node --harmony-async-await getExample.js

ง่ายที่สุด!


8

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

ตามวิธีการในห่วงโซ่ -javascript- สัญญา

ตกลงลองดูรหัส:

const firstPromise = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(secondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });

4
สิ่งนี้ไม่ได้ตอบคำถามเกี่ยวกับวิธีเข้าถึงผลลัพธ์ก่อนหน้าในเชน
Bergi

2
ทุกสัญญาจะได้รับคุณค่าก่อนหน้าความหมายของคุณคืออะไร?
yzfdjzwl

1
ลองดูรหัสในคำถาม เป้าหมายไม่ได้รับผลลัพธ์ของสัญญาที่.thenเรียกใช้ แต่เป็นผลลัพธ์จากก่อนหน้านั้น เช่นการเข้าถึงผลมาจากการthirdPromise firstPromise
Bergi

6

คำตอบอื่นโดยใช้babel-nodeเวอร์ชัน <6

การใช้ async - await

npm install -g babel@5.6.14

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

จากนั้นเรียกใช้babel-node example.jsและ voila!


1
ใช่ฉันทำทันทีหลังจากโพสต์ของฉัน แต่ถึงกระนั้นฉันจะออกจากมันเพราะมันจะอธิบายวิธีการเริ่มต้นและใช้งานกับการใช้ ES7 จริงเมื่อเทียบกับเพียงแค่บอกว่าสักวันหนึ่ง ES7 จะสามารถใช้ได้
แอนโธนี

1
โอ้ถูกต้องฉันควรอัปเดตคำตอบเพื่อบอกว่าปลั๊กอิน "ทดลอง" สำหรับสิ่งเหล่านี้อยู่ที่นี่แล้ว
Bergi

2

ฉันจะไม่ใช้รูปแบบนี้ในรหัสของตัวเองเพราะฉันไม่ใช่แฟนตัวยงของการใช้ตัวแปรทั่วโลก อย่างไรก็ตามในการเหน็บแนมมันจะทำงาน

ผู้ใช้เป็นรุ่น Mongoose ที่ได้รับการแนะนำ

var globalVar = '';

User.findAsync({}).then(function(users){
  globalVar = users;
}).then(function(){
  console.log(globalVar);
});

2
โปรดสังเกตว่ารูปแบบนี้มีรายละเอียดอยู่แล้วในคำตอบของบริบทที่ไม่แน่นอน (และทำไมมันน่าเกลียด - ฉันไม่ใช่แฟนตัวยง)
Bergi

ในกรณีของคุณรูปแบบดูเหมือนจะไร้ประโยชน์แม้ว่า คุณไม่จำเป็นต้องมีglobalVarที่ทุกคนเพียงแค่ทำUser.findAsync({}).then(function(users){ console.log(users); mongoose.connection.close() });?
Bergi

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

2

คำตอบอื่นโดยใช้nsynjsผู้ปฏิบัติการตามลำดับ:

function getExample(){

  var response1 = returnPromise1().data;

  // promise1 is resolved at this point, '.data' has the result from resolve(result)

  var response2 = returnPromise2().data;

  // promise2 is resolved at this point, '.data' has the result from resolve(result)

  console.log(response, response2);

}

nynjs.run(getExample,{},function(){
    console.log('all done');
})

ปรับปรุง: เพิ่มตัวอย่างการทำงาน

function synchronousCode() {
     var urls=[
         "https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
     ];
     for(var i=0; i<urls.length; i++) {
         var len=window.fetch(urls[i]).data.text().data.length;
         //             ^                   ^
         //             |                   +- 2-nd promise result
         //             |                      assigned to 'data'
         //             |
         //             +-- 1-st promise result assigned to 'data'
         //
         console.log('URL #'+i+' : '+urls[i]+", length: "+len);
     }
}

nsynjs.run(synchronousCode,{},function(){
    console.log('all done');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>


1

เมื่อใช้ Bluebird คุณสามารถใช้.bindวิธีการแบ่งปันตัวแปรในสัญญาเชน:

somethingAsync().bind({})
.spread(function (aValue, bValue) {
    this.aValue = aValue;
    this.bValue = bValue;
    return somethingElseAsync(aValue, bValue);
})
.then(function (cValue) {
    return this.aValue + this.bValue + cValue;
});

โปรดตรวจสอบลิงค์นี้สำหรับข้อมูลเพิ่มเติม:

http://bluebirdjs.com/docs/api/promise.bind.html


ขอให้สังเกตว่ารูปแบบนี้มีรายละเอียดอยู่แล้วในคำตอบของบริบทที่ไม่แน่นอน
Bergi

1
function getExample() {
    var retA, retB;
    return promiseA(…).then(function(resultA) {
        retA = resultA;
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        //retA is value of promiseA
        return // How do I gain access to resultA here?
    });
}

วิธีง่ายๆ: D


คุณสังเกตเห็นคำตอบนี้หรือไม่?
Bergi

1

ฉันคิดว่าคุณสามารถใช้แฮชของ RSVP ได้

สิ่งที่ชอบด้านล่าง:

    const mainPromise = () => {
        const promise1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('first promise is completed');
                resolve({data: '123'});
            }, 2000);
        });

        const promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('second promise is completed');
                resolve({data: '456'});
            }, 2000);
        });

        return new RSVP.hash({
              prom1: promise1,
              prom2: promise2
          });

    };


   mainPromise()
    .then(data => {
        console.log(data.prom1);
        console.log(data.prom2);
    });

ใช่นั่นเป็นวิธีการPromise.allแก้ปัญหาเฉพาะกับวัตถุแทนอาร์เรย์
Bergi

0

สารละลาย:

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

นี่คือตัวอย่างที่สมบูรณ์:

// Get info asynchronously from a server
function pGetServerInfo()
    {
    // then value: "server info"
    } // pGetServerInfo

// Write into a file asynchronously
function pWriteFile(path,string)
    {
    // no then value
    } // pWriteFile

// The heart of the solution: Write formatted info into a log file asynchronously,
// using the pGetServerInfo and pWriteFile operations
function pLogInfo(localInfo)
    {
    var scope={localInfo:localInfo}; // Create an explicit scope object
    var thenFunc=p2.bind(scope); // Create a temporary function with this scope
    return (pGetServerInfo().then(thenFunc)); // Do the next 'then' in the chain
    } // pLogInfo

// Scope of this 'then' function is {localInfo:localInfo}
function p2(serverInfo)
    {
    // Do the final 'then' in the chain: Writes "local info, server info"
    return pWriteFile('log',this.localInfo+','+serverInfo);
    } // p2

วิธีการแก้ปัญหานี้สามารถเรียกใช้ดังนี้

pLogInfo("local info").then().catch(err);

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


นี่ดูเหมือนจะเป็นรูปแบบเดียวกับที่อยู่ในรังคำตอบ(และ) การปิด
Bergi

มันดูคล้ายกัน ฉันได้เรียนรู้ว่าไวยากรณ์ Async / Await ใหม่รวมถึงการรวมการเชื่อมโยงของอาร์กิวเมนต์อัตโนมัติดังนั้นข้อโต้แย้งทั้งหมดจึงมีอยู่ในฟังก์ชั่นอะซิงโครนัสทั้งหมด ฉันกำลังละทิ้งสัญญา
David Spector

async/ awaitยังหมายถึงการใช้สัญญา สิ่งที่คุณอาจทิ้งไว้คือthenโทรออกด้วยการโทรกลับ
Bergi

-1

สิ่งที่ฉันเรียนรู้เกี่ยวกับสัญญาคือใช้มันเฉพาะเมื่อค่าส่งคืนหลีกเลี่ยงการอ้างอิงพวกเขาหากเป็นไปได้ ไวยากรณ์ async / await เป็นประโยชน์อย่างยิ่งสำหรับการที่ ทุกวันนี้เบราว์เซอร์และโหนดทั้งหมดรองรับ: https://caniuse.com/#feat=async-functionsเป็นพฤติกรรมที่เรียบง่ายและรหัสก็เหมือนกับการอ่านรหัสซิงโครนัสลืมเกี่ยวกับการโทรกลับ ...

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

/**
 * Promise like object that allows to resolve it promise from outside code. Example:
 *
```
class Api {
  fooReady = new Deferred<Data>()
  private knower() {
    inOtherMoment(data=>{
      this.fooReady.resolve(data)
    })
  }
}
```
 */
var Deferred = /** @class */ (function () {
  function Deferred(callback) {
    var instance = this;
    this.resolve = null;
    this.reject = null;
    this.status = 'pending';
    this.promise = new Promise(function (resolve, reject) {
      instance.resolve = function () { this.status = 'resolved'; resolve.apply(this, arguments); };
      instance.reject = function () { this.status = 'rejected'; reject.apply(this, arguments); };
    });
    if (typeof callback === 'function') {
      callback.call(this, this.resolve, this.reject);
    }
  }
  Deferred.prototype.then = function (resolve) {
    return this.promise.then(resolve);
  };
  Deferred.prototype.catch = function (r) {
    return this.promise.catch(r);
  };
  return Deferred;
}());

รูปแบบ transpiled โครงการ typescript ของฉัน:

https://github.com/cancerberoSgx/misc-utils-of-mine/blob/2927c2477839f7b36247d054e7e50abe8a41358b/misc-utils-of-mine-generic/src/promise.ts#L31

สำหรับกรณีที่ซับซ้อนมากขึ้นฉันมักจะใช้โปรแกรมอรรถประโยชน์สัญญาขนาดเล็กเหล่านี้โดยไม่ต้องทดสอบและพิมพ์อ้างอิง p-map มีประโยชน์หลายครั้ง ฉันคิดว่าเขาครอบคลุมกรณีการใช้งานส่วนใหญ่:

https://github.com/sindresorhus?utf8=%E2%9C%93&tab=repositories&q=promise&type=source&language=


ดูเหมือนว่าคุณกำลังแนะนำ สถานะบริบทที่ไม่แน่นอนหรือการตรวจสอบแบบซิงโครนัสหรือไม่?
Bergi

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