การแก้คำสัญญาหลาย ๆ ครั้งปลอดภัยหรือไม่?


115

ฉันมีบริการ i18n ในแอปพลิเคชันของฉันซึ่งมีรหัสต่อไปนี้:

var i18nService = function() {
  this.ensureLocaleIsLoaded = function() {
    if( !this.existingPromise ) {
      this.existingPromise = $q.defer();

      var deferred = this.existingPromise;
      var userLanguage = $( "body" ).data( "language" );
      this.userLanguage = userLanguage;

      console.log( "Loading locale '" + userLanguage + "' from server..." );
      $http( { method:"get", url:"/i18n/" + userLanguage, cache:true } ).success( function( translations ) {
        $rootScope.i18n = translations;
        deferred.resolve( $rootScope.i18n );
      } );
    }

    if( $rootScope.i18n ) {
      this.existingPromise.resolve( $rootScope.i18n );
    }

    return this.existingPromise.promise;
  };

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

ขณะนี้ฉันเพิ่งจัดเก็บคำสัญญาเดียวและแก้ไขหากผู้ใช้เรียกใช้ฟังก์ชันนี้อีกครั้งหลังจากเรียกข้อมูลโลแคลจากเซิร์ฟเวอร์สำเร็จแล้ว

จากสิ่งที่ฉันสามารถบอกได้ว่านี่ทำงานได้ตามที่ตั้งใจไว้ แต่ฉันสงสัยว่านี่เป็นแนวทางที่เหมาะสมหรือไม่


7
ดูคำตอบนี้
robertklep

ฉันก็ใช้มันและมันก็ใช้ได้ดีเช่นกัน
Chandermani

คำตอบ:


119

ตามที่ฉันเข้าใจคำสัญญาในปัจจุบันนี้ควรจะปรับ 100% สิ่งเดียวที่ต้องเข้าใจคือเมื่อได้รับการแก้ไขแล้ว (หรือปฏิเสธ) นั่นคือสำหรับวัตถุที่คล้อยตาม - เสร็จสิ้น

หากคุณควรเรียกร้องthen(...)สัญญาอีกครั้งคุณควรได้รับผลการแก้ไข / ปฏิเสธ (ครั้งแรก) ทันที

การโทรเพิ่มเติมresolve()จะไม่ (ไม่ควร?) มีผลใด ๆ ไม่แน่ใจว่าจะเกิดอะไรขึ้นหากคุณพยายามrejectใช้วัตถุที่ถูกเลื่อนออกไปก่อนหน้านี้resolved(ฉันสงสัยว่าไม่มีอะไร)


28
นี่คือ JSBin ที่แสดงให้เห็นว่าสิ่งที่กล่าวมาทั้งหมดเป็นจริง: jsbin.com/gemepay/3/edit?js,consoleใช้การแก้ปัญหาครั้งแรกเท่านั้น
konrad

4
มีใครพบเอกสารอย่างเป็นทางการเกี่ยวกับเรื่องนี้บ้าง? โดยทั่วไปเป็นเรื่องที่หลีกเลี่ยงไม่ได้ที่จะอาศัยพฤติกรรมที่ไม่มีเอกสารแม้ว่าจะใช้งานได้ในขณะนี้ก็ตาม
3ocene

ecma-international.org/ecma-262/6.0/#sec-promise.resolve - ฉันต้องลงวันที่ไม่พบสิ่งใดที่ระบุว่าโดยเนื้อแท้แล้ว UNSAFE หากตัวจัดการของคุณทำสิ่งที่ควรทำครั้งเดียวจริงๆฉันจะให้มันตรวจสอบและอัปเดตสถานะบางอย่างก่อนที่จะดำเนินการอีกครั้ง แต่ฉันต้องการรายการ MDN อย่างเป็นทางการหรือเอกสารข้อมูลจำเพาะเพื่อให้ได้ความชัดเจนอย่างแท้จริง
demaniak

ฉันไม่เห็นอะไร "หนักใจ" ในหน้า PromiseA + ดูpromisesaplus.com
demaniak

3
@demaniak คำถามนี้เกี่ยวกับสัญญา / A +ไม่ใช่สัญญา ES6 แต่การที่จะตอบคำถามของคุณเป็นส่วนหนึ่งของข้อมูลจำเพาะ ES6 เกี่ยวกับการแก้ปัญหาภายนอก / การปฏิเสธเป็นที่ปลอดภัยเป็นที่นี่
Trevor Robinson

1

ฉันเผชิญกับสิ่งเดียวกันเมื่อไม่นานมานี้คำสัญญาสามารถแก้ไขได้เพียงครั้งเดียวการพยายามอีกครั้งจะไม่ทำอะไรเลย (ไม่มีข้อผิดพลาดไม่มีคำเตือนไม่มีthenการร้องขอ)

ฉันตัดสินใจที่จะแก้ไขปัญหานี้:

getUsers(users => showThem(users));

getUsers(callback){
    callback(getCachedUsers())
    api.getUsers().then(users => callback(users))
}

เพียงแค่ส่งฟังก์ชั่นของคุณเป็นการโทรกลับและเรียกใช้หลาย ๆ ครั้งที่คุณต้องการ! หวังว่าจะสมเหตุสมผล


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

@JohnHenckel แนวคิดคือการแก้ไขคำสัญญาหลาย ๆ ครั้งคือส่งคืนข้อมูลหลายครั้งไม่ต้องมีหลาย .thenคำสั่ง สำหรับสิ่งที่คุ้มค่าฉันคิดว่าวิธีเดียวที่จะส่งคืนข้อมูลหลาย ๆ ครั้งไปยังบริบทการโทรคือการใช้การโทรกลับไม่ใช่คำสัญญาเนื่องจากสัญญาไม่ได้ถูกสร้างขึ้นเพื่อให้ทำงานในลักษณะนั้น
T. Rex

0

หากคุณต้องการเปลี่ยนค่าที่ส่งคืนของคำสัญญาเพียงแค่ส่งคืนค่าใหม่ในthenและเชื่อมโยงถัดไปthen/ catchบน

var p1 = new Promise((resolve, reject) => { resolve(1) });
    
var p2 = p1.then(v => {
  console.log("First then, value is", v);
  return 2;
});
    
p2.then(v => {
  console.log("Second then, value is", v);
});


0

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

const evokeObjectMethodWithArgs = (methodName, args) => (src) => src[methodName].apply(null, args);
    const hasMethodName = (name) => (target = {}) => typeof target[name] === 'function';
    const Observable = function (fn) {
        const subscribers = [];
        this.subscribe = subscribers.push.bind(subscribers);
        const observer = {
            next: (...args) => subscribers.filter(hasMethodName('next')).forEach(evokeObjectMethodWithArgs('next', args))
        };
        setTimeout(() => {
            try {
                fn(observer);
            } catch (e) {
                subscribers.filter(hasMethodName('error')).forEach(evokeObjectMethodWithArgs('error', e));
            }
        });

    };

    const fromEvent = (target, eventName) => new Observable((obs) => target.on(eventName, obs.next));

    fromEvent(client, 'document:save').subscribe({
        async next(document, docName) {
            await writeFilePromise(resolve(dataDir, `${docName}`), document);
            client.emit('document:save', document);
        }
    });

0

คุณสามารถเขียนการทดสอบเพื่อยืนยันพฤติกรรม

โดยทำการทดสอบต่อไปนี้คุณสามารถสรุปได้

การโทรแก้ไข () / ปฏิเสธ () ไม่เคยเกิดข้อผิดพลาด

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

คุณยังสามารถตรวจสอบโพสต์บล็อกของฉันเพื่อดูรายละเอียด

/* eslint-disable prefer-promise-reject-errors */
const flipPromise = require('flip-promise').default

describe('promise', () => {
    test('error catch with resolve', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise(resolve => {
            try {
                resolve()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
            throw new Error('error thrown out side')
        } catch (e) {
            rs('error caught in expected location')
        }
    }))
    test('error catch with reject', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise((_resolve, reject) => {
            try {
                reject()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
        } catch (e) {
            try {
                throw new Error('error thrown out side')
            } catch (e){
                rs('error caught in expected location')
            }
        }
    }))
    test('await multiple times resolved promise', async () => {
        const pr = Promise.resolve(1)
        expect(await pr).toBe(1)
        expect(await pr).toBe(1)
    })
    test('await multiple times rejected promise', async () => {
        const pr = Promise.reject(1)
        expect(await flipPromise(pr)).toBe(1)
        expect(await flipPromise(pr)).toBe(1)
    })
    test('resolve multiple times', async () => {
        const pr = new Promise(resolve => {
            resolve(1)
            resolve(2)
            resolve(3)
        })
        expect(await pr).toBe(1)
    })
    test('resolve then reject', async () => {
        const pr = new Promise((resolve, reject) => {
            resolve(1)
            resolve(2)
            resolve(3)
            reject(4)
        })
        expect(await pr).toBe(1)
    })
    test('reject multiple times', async () => {
        const pr = new Promise((_resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
        })
        expect(await flipPromise(pr)).toBe(1)
    })

    test('reject then resolve', async () => {
        const pr = new Promise((resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
            resolve(4)
        })
        expect(await flipPromise(pr)).toBe(1)
    })
test('constructor is not async', async () => {
    let val
    let val1
    const pr = new Promise(resolve => {
        val = 1
        setTimeout(() => {
            resolve()
            val1 = 2
        })
    })
    expect(val).toBe(1)
    expect(val1).toBeUndefined()
    await pr
    expect(val).toBe(1)
    expect(val1).toBe(2)
})

})

-1

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

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