AngularJS: จะใช้สัญญาได้ที่ไหน


141

ฉันเห็นตัวอย่างของบริการเข้าสู่ระบบ Facebook ที่ใช้สัญญาเพื่อเข้าถึง FB Graph API

ตัวอย่าง # 1 :

this.api = function(item) {
  var deferred = $q.defer();
  if (item) {
    facebook.FB.api('/' + item, function (result) {
      $rootScope.$apply(function () {
        if (angular.isUndefined(result.error)) {
          deferred.resolve(result);
        } else {
          deferred.reject(result.error);
        }
      });
    });
  }
  return deferred.promise;
}

และบริการที่ใช้"$scope.$digest() // Manual scope evaluation"เมื่อได้รับการตอบสนอง

ตัวอย่างที่ 2 :

angular.module('HomePageModule', []).factory('facebookConnect', function() {
    return new function() {
        this.askFacebookForAuthentication = function(fail, success) {
            FB.login(function(response) {
                if (response.authResponse) {
                    FB.api('/me', success);
                } else {
                    fail('User cancelled login or did not fully authorize.');
                }
            });
        }
    }
});

function ConnectCtrl(facebookConnect, $scope, $resource) {

    $scope.user = {}
    $scope.error = null;

    $scope.registerWithFacebook = function() {
        facebookConnect.askFacebookForAuthentication(
        function(reason) { // fail
            $scope.error = reason;
        }, function(user) { // success
            $scope.user = user
            $scope.$digest() // Manual scope evaluation
        });
    }
}

JSFiddle

คำถามคือ:

  • ความแตกต่างในตัวอย่างด้านบนคืออะไร?
  • อะไรคือเหตุผลและกรณีในการใช้บริการ$ q
  • และมันทำงานอย่างไร

9
ฟังดูเหมือนคุณควรอ่านสิ่งที่สัญญาคืออะไรและทำไมพวกเขาถึงใช้งานทั่วไป ... พวกเขาไม่ได้ จำกัด เฉพาะมุมและมีวัสดุมากมายให้ใช้งาน
charlietfl

1
@ charlietfl จุดดี แต่ฉันคาดหวังคำตอบที่ซับซ้อนที่จะครอบคลุมทั้งสอง: ทำไมพวกเขาถูกนำมาใช้โดยทั่วไปและวิธีการใช้ในเชิงมุม ขอบคุณสำหรับคำแนะนำของคุณ
Maksym

คำตอบ:


401

นี่ไม่ใช่คำตอบที่สมบูรณ์สำหรับคำถามของคุณ แต่หวังว่าจะช่วยให้คุณและผู้อื่นเมื่อคุณพยายามอ่านเอกสารใน$qบริการ ฉันใช้เวลาสักครู่เพื่อทำความเข้าใจ

ลองแยก AngularJS สักครู่แล้วลองพิจารณาการโทรผ่าน Facebook API การเรียก API ทั้งสองใช้กลไกการโทรกลับเพื่อแจ้งผู้โทรเมื่อมีการตอบกลับจาก Facebook:

  facebook.FB.api('/' + item, function (result) {
    if (result.error) {
      // handle error
    } else {
      // handle success
    }
  });
  // program continues while request is pending
  ...

นี่เป็นรูปแบบมาตรฐานสำหรับการจัดการการดำเนินการแบบอะซิงโครนัสใน JavaScript และภาษาอื่น ๆ

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

  FB.login(function(response) {
      if (response.authResponse) {
          FB.api('/me', success);
      } else {
          fail('User cancelled login or did not fully authorize.');
      }
  });

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

แม้ในกรณีนี้ซึ่งเป็นการผูกมัดเพียงสองอย่างเข้าด้วยกันสิ่งต่าง ๆ ก็เริ่มยุ่งเหยิง วิธีนี้askFacebookForAuthenticationยอมรับการเรียกกลับสำหรับความล้มเหลวและความสำเร็จ แต่เกิดอะไรขึ้นเมื่อFB.loginสำเร็จ แต่FB.apiล้มเหลว วิธีการนี้จะเรียกการsuccessเรียกกลับโดยไม่คำนึงถึงผลของFB.apiวิธีการ

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

ทีนี้ลองแยก Facebook API สักครู่แล้วลองพิจารณา Angular Promises API ตามที่$qบริการดำเนินการ รูปแบบที่ดำเนินการโดยบริการนี้เป็นความพยายามที่จะเปลี่ยนการเขียนโปรแกรมแบบอะซิงโครนัสกลับไปเป็นสิ่งที่คล้ายกับชุดข้อความเชิงเส้นตรงซึ่งมีความสามารถในการ 'โยน' ข้อผิดพลาดในขั้นตอนใด ๆtry/catchบล็อกที่คุ้นเคย

ลองพิจารณาตัวอย่างที่ประดิษฐ์ขึ้นนี้ สมมติว่าเรามีสองฟังก์ชันโดยที่ฟังก์ชันที่สองใช้ผลลัพธ์ของฟังก์ชันแรก:

 var firstFn = function(param) {
    // do something with param
    return 'firstResult';
 };

 var secondFn = function(param) {
    // do something with param
    return 'secondResult';
 };

 secondFn(firstFn()); 

ทีนี้ลองนึกภาพว่า firstFn และ secondFn ทั้งคู่ใช้เวลานานกว่าจะเสร็จสมบูรณ์ดังนั้นเราจึงต้องการประมวลผลลำดับนี้แบบอะซิงโครนัส ครั้งแรกที่เราสร้างdeferredวัตถุใหม่ซึ่งแสดงถึงห่วงโซ่ของการดำเนินการ:

 var deferred = $q.defer();
 var promise = deferred.promise;

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

จนถึงคำสัญญาของเราเพียงแสดงจุดเริ่มต้นในห่วงโซ่ ทีนี้ลองเพิ่มการดำเนินการสองอย่างของเรา:

 promise = promise.then(firstFn).then(secondFn);

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

จนถึงตอนนี้เราได้ตั้งค่าฟังก์ชั่นโซ่ แต่ก็ไม่มีอะไรเกิดขึ้นจริง คุณได้รับสิ่งที่เริ่มต้นด้วยการโทรdeferred.resolveระบุค่าเริ่มต้นที่คุณต้องการผ่านไปยังขั้นตอนแรกที่แท้จริงในห่วงโซ่:

 deferred.resolve('initial value');

แล้ว ... ยังไม่มีอะไรเกิดขึ้น เพื่อให้แน่ใจว่ามีการตรวจสอบการเปลี่ยนแปลงรูปแบบอย่างเหมาะสม Angular ไม่ได้เรียกขั้นตอนแรกในห่วงโซ่จริงจนกว่า$applyจะมีการเรียกใช้ครั้งต่อไป:

 deferred.resolve('initial value');
 $rootScope.$apply();

 // or     
 $rootScope.$apply(function() {
    deferred.resolve('initial value');
 });

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

 var firstFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'firstResult';
    }
 };

 var secondFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'secondResult';
    }
 };

 var thirdFn = function(param) {
    // do something with param
    return 'thirdResult';
 };

 var errorFn = function(message) {
   // handle error
 };

 var deferred = $q.defer();
 var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);

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

หากต้องการกลับไปที่ตัวอย่างของคุณ (และคำถามของคุณ) อย่างรวดเร็วฉันจะบอกว่าพวกเขาเป็นตัวแทนของสองวิธีที่แตกต่างกันในการปรับ API การติดต่อกลับของ Facebook ให้สอดคล้องกับวิธีของ Angular ในการสังเกตการเปลี่ยนแปลงรูปแบบ ตัวอย่างแรกล้อมรอบการเรียก API ในสัญญาซึ่งสามารถเพิ่มในขอบเขตและเข้าใจได้โดยระบบ templating ของ Angular ครั้งที่สองใช้วิธีการบังคับเดรัจฉานมากขึ้นในการตั้งค่าผลลัพธ์การโทรกลับโดยตรงบนขอบเขตแล้วเรียก$scope.$digest()ให้ Angular รับรู้การเปลี่ยนแปลงจากแหล่งภายนอก

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


5
ฉันคิดว่ามันเป็นคำตอบที่ยอดเยี่ยม! สิ่งสำคัญสำหรับฉันคืออธิบายกรณีทั่วไปเมื่อสัญญาเป็นเรื่องจริง จริง ๆ แล้วฉันก็หวังว่าจะเป็นตัวอย่างจริง (เช่นเดียวกับ Facebook) แต่นี่ก็ใช้งานได้เช่นกัน ขอบคุณมาก!
Maksym

2
ทางเลือกในการผูกมัดหลายวิธีคือการใช้งานthen $q.allกวดวิชาอย่างรวดเร็วในที่สามารถพบได้ที่นี่
Bogdan

2
$q.allมีความเหมาะสมถ้าคุณต้องรอให้การดำเนินการแบบอะซิงโครนัสอิสระหลายรายการเสร็จสิ้น มันไม่ได้แทนที่การผูกมัดหากแต่ละการดำเนินการขึ้นอยู่กับผลลัพธ์ของการดำเนินการก่อนหน้า
karlgold

1
การผูกมัดนั้นอธิบายไว้อย่างชัดเจนที่นี่ ช่วยให้ฉันเข้าใจและใช้งานให้เต็มศักยภาพ ขอบคุณ
Tushar Joshi

1
คำตอบที่ดี @karlgold! ฉันมีหนึ่งคำถาม. หากในข้อมูลโค้ดสุดท้ายคุณเปลี่ยนreturn 'firstResult'ส่วนเป็นreturn $q.resolve('firstResult')อะไรจะแตกต่างกันอย่างไร
technophyle

9

ฉันคาดหวังคำตอบที่ซับซ้อนที่จะครอบคลุมทั้งสอง: ทำไมจึงใช้งานทั่วไปและวิธีใช้ใน Angular

นี่คือเสียงทุ้มสำหรับMVP สัญญาเชิงมุม (สัญญา ขั้นต่ำปฏิบัติ) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

ที่มา:

(สำหรับผู้ที่ขี้เกียจคลิกที่ลิงค์)

index.html

  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-app="myModule" ng-controller="HelloCtrl">
    <h1>Messages</h1>
    <ul>
      <li ng-repeat="message in messages">{{ message }}</li>
    </ul>
  </body>

</html>

app.js

angular.module('myModule', [])

  .factory('HelloWorld', function($q, $timeout) {

    var getMessages = function() {
      var deferred = $q.defer();

      $timeout(function() {
        deferred.resolve(['Hello', 'world']);
      }, 2000);

      return deferred.promise;
    };

    return {
      getMessages: getMessages
    };

  })

  .controller('HelloCtrl', function($scope, HelloWorld) {

    $scope.messages = HelloWorld.getMessages();

  });

(ฉันรู้ว่ามันไม่ได้แก้ปัญหาตัวอย่าง Facebook ของคุณโดยเฉพาะ แต่ฉันพบว่าข้อมูลโค้ดต่อไปนี้มีประโยชน์)

Via: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


อัปเดต 28 กุมภาพันธ์ 2557: ตั้งแต่วันที่ 1.2.0 สัญญาจะไม่ได้รับการแก้ไขโดยแม่แบบอีกต่อไป http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(ตัวอย่างตัวเรียงเสียงใช้ 1.1.5)


afaik พวกเรารักกันมากเพราะพวกเราขี้เกียจ
mkb

สิ่งนี้ช่วยให้ฉันเข้าใจ $ q, deferred และ chained. แล้วเรียกใช้ฟังก์ชันดังนั้นขอบคุณ
aliopi

1

การรอการตัดบัญชีแสดงถึงผลลัพธ์ของการดำเนินการแบบอะซินทรอนิกส์ มันเปิดเผยอินเตอร์เฟสที่สามารถใช้สำหรับการส่งสัญญาณสถานะและผลลัพธ์ของการดำเนินการที่แสดง นอกจากนี้ยังมีวิธีรับอินสแตนซ์สัญญาที่เกี่ยวข้อง

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

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


1

ใช้สัญญาภายในตัวควบคุมและตรวจสอบให้แน่ใจว่ามีข้อมูลหรือไม่

 var app = angular.module("app",[]);
      
      app.controller("test",function($scope,$q){
        var deferred = $q.defer();
        deferred.resolve("Hi");
        deferred.promise.then(function(data){
        console.log(data);    
        })
      });
      angular.bootstrap(document,["app"]);
<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
  </head>

  <body>
    <h1>Hello Angular</h1>
    <div ng-controller="test">
    </div>
  </body>

</html>

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