關於 Angular 裏的 $q 和 Promise

什麼是 Promise?Angular 裏的 $q 和 Promise 究竟是怎麼樣一種關係?在 Angular 裏使用 $q 究竟是怎麼樣一種體驗?本文將爲你一一解答。javascript

原文連接:All about $q and Promises in Angular
原文做者:Todd Motto
譯者:linpu.lijava

可能你曾經見過 $q,或許已經在使用它了,可是可能你還沒發現它有一些很棒的特性,好比 $q.all()$q.race()。這篇文章將深刻 ES2015 的 Promise API 以及它是如何映射到 Angular 裏的 $q 。換句話說,這篇文章所有是關於 $q 的,但願你能喜歡!git

什麼是 Promise?

Promise 是一個特殊類型對象,咱們能夠直接使用或者構造新的實例來處理一個異步任務。把它稱做 Promise 是由於咱們被「承諾」了在將來的某個時間點會獲得一個結果。好比一個 HTTP 調用會在 200ms 或者 400ms 後完成,當完成以後就會有一個 Promise 執行。angularjs

一個 Promise 有三種狀態:pending、resolved 和 rejected。在 Angular 裏使用 $q 能夠構造一個新的 Promise,可是先讓咱們來看看 ES2015 裏的 Promise 對象來熟悉一下怎麼建立它。github

ES2015 裏的 Promise

這裏主要的內容是 Promise 和做爲參數的 resolverejectapi

let promise = new Promise((resolve, reject) => {
  if (/* 某個異步任務順利執行 */) {
    resolve('Success!');
  } else {
    reject('Oops... something went wrong');
  }
});

promise.then(data => {
  console.log(data);
});複製代碼

咱們簡單地調用了 new Promise(),在其內部能夠執行一個異步任務,這個任務可能封裝了一個特別的 DOM 事件,或者甚至是封裝了一些不是 Promise 對象的第三方庫。數組

舉個例子,封裝一個假定的第三方庫,叫作 myCallbackLib(),它將提供一個 success 和 error 回調函數,咱們能夠在這個方法上構造一個 Promise,而後在相對應的位置去 resolvereject 結果:promise

const handleThirdPartyCallback = someArgument => {
  let promise = new Promise((resolve, reject) => {
    // 假定某些不是 Promise 對象的第三方庫接口
    // 但調用完成後會執行回調函數
    myCallbackLib(someArgument, response => {
      // we can resolve it with the response
      resolve(response);
    }, reason => {
      // we can reject it with the reason
      reject(reason);
    });
  });
  return promise;
};

handleThirdPartyCallback({ user: 101 }).then(data => {
  console.log(data);
});複製代碼

$q 構造函數

AngularJS 裏的 $q 實現如今已經和原生的 ES2015 Promise 對象同樣了,因此咱們能夠這麼寫:app

let promise = $q((resolve, reject) => {
  if (/* 某個異步任務順利執行 */) {
    resolve('Success!');
  } else {
    reject('Oops... something went wrong');
  }
});

promise.then(data => {
  console.log(data);
});複製代碼

和以前代碼惟一的區別就在於將 new Promise() 改爲了 $q,變得足夠簡單了。異步

更理想的狀況是在一個 service 裏實現它:

function MyService($q) {
  return {
    getSomething() {
      return $q((resolve, reject) => {
        if (/* 某個異步任務順利執行 */) {
          resolve('Success!');
        } else {
          reject('Oops... something went wrong');
        }
      });
    }
  };
}

angular
  .module('app')
  .service('MyService', MyService);複製代碼

以後就能夠將它注入一個 component 控制器

const stuffComponent = {
  template: ` <div> {{ $ctrl.stuff }} </div> `,
  controller(MyService) {
    this.stuff = [];
    MyService.getSomething()
      .then(data => this.stuff.unshift(data));
  }
};

angular
  .module('app')
  .component('stuffComponent', stuffComponent);複製代碼

或者做爲一個 bindings 屬性在一個路由組件中使用,並映射到一個路由處理對象

const stuffComponent = {
  bindings: {
    stuff: '<'
  },
  template: ` <div> {{ $ctrl.stuff }} </div> `,
  controller(MyService) {
    // your stuff already available
    console.log(this.stuff);
  }
};

const config = $stateProvider => {
  $stateProvider
    .state('stuff', {
      url: '/stuff',
      component: 'stuffComponent',
      resolve: {
        // resolve maps the `MyService` promise response
        // Object across to `stuff` property, making it
        // available as a binding inside the .component()
        stuff: MyService => MyService.getSomething()
      }
    });
};

angular
  .module('app')
  .config(config)
  .component('stuffComponent', stuffComponent);複製代碼

何時使用 $q

目前爲止咱們都只是看了一些假定的例子,下面的實現是我將一個 XMLHttpRequest 對象封裝成了一個基於 Promise 的解決方案,這種類型的實現應該是你建立本身的 $q Promise 僅有的真實緣由吧:

let getStuff = $q((resolve, reject) => {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(JSON.parse(xhr.responseText));
      }
    }
  };
  xhr.open('GET', '/api/stuff', true);
  xhr.send();
});

getStuff.then(data => {
  console.log('Boom!', data);
});複製代碼

請注意,這並非在倡導你建立和使用 XMLHttpRequest,在 Angular 裏就使用 $http,它已經爲你建立並返回了一個 Promise 對象:

function getStuff() {
  return $http
    .get('/api/stuff');
    .then(data => {
      console.log('Boom!', data);
    });
}

getStuff().then(data => {
  console.log('Boom!', data);
});複製代碼

這還意味着,你不能並且不該該像下面這樣作,由於下面至關於從一個已經存在的 Promise 對象裏建立一個 Promise 對象:

function getStuff() {
  // 不要這麼作!
  let defer = $q.defer();
  $http
    .get('/api/stuff');
    .then(response => {
      // 不要這麼作!
      $q.resolve(response);
    }, reason => {
      // 不要這麼作!
      $q.reject(reason);
    });
  return defer.promise;
}

getStuff().then(data => {
  console.log('Boom!', data);
});複製代碼

黃金法則:只對自己沒有 Promise 的東西使用 $q!雖然只在這種狀況下建立 Promise,可是你能夠對其餘的 Promise 使用一些其餘的方法,好比 $q.all()$q.race()

$q.defer()

使用 $q.defer() 只是 $q 做爲構造函數的另一種風格和原始實現。假設下面的代碼,改自以前使用 service 的例子:

function MyService($q) {
  return {
    getSomething() {
      let defer = $q.defer();
      if (/* some async task is all good */) {
        defer.resolve('Success!');
      } else {
        defer.reject('Oops... something went wrong');
      }
      return defer.promise;
    }
  };
}複製代碼

$q.when() / $q.resolve()

當你想要當即從一個非 Promise 對象中處理一個 Promise 的時候就可使用 $q.when() 或者 $q.resolve()(它們是同樣的,$q.resolve()$q.when() 的一個別名,爲了符合 ES2015 Promise 的命名約定),舉個例子:

$q.when(123).then(res => {
  // 123
  console.log(res);
});

$q.resolve(123).then(res => {
  // 123
  console.log(res);
});複製代碼

注意:$q.when() 也是和 $q.resolve() 同樣的。

$q.reject()

使用 $q.reject() 會當即拒絕掉一個 Promise,這麼作是爲了方便一些狀況作處理,好比在 HTTP 攔截器沒有任何返回的時候,就能夠返回一個拒絕掉的 Promise 對象:

$httpProvider.interceptors.push($q => ({
  request(config) {...},
  requestError(config) {
    return $q.reject(config);
  },
  response(response) {...},
  responseError(response) {
    return $q.reject(response);
  }
}));複製代碼

$q.all()

有的時候你可能須要一次性處理多個 Promise,經過 $q.all() 就能夠輕易實現,只需傳遞進 Promise 的數組或者對象,接着就會在全部 Promise 都處理完後調用 .then() 方法:

let promiseOne = $http.get('/api/todos');
let promiseTwo = $http.get('/api/comments');

// Promise 數組
$q.all([promiseOne, promiseTwo]).then(data => {
  console.log('Both promises have resolved', data);
});

// Promise 對象哈希
// 這是 ES2015 對 { promiseOne: promiseOne, promiseTwo: promiseTwo } 的簡寫
$q.all({
    promiseOne,
    promiseTwo
  }).then(data => {
  console.log('Both promises have resolved', data);
});複製代碼

$q.race()

$q.race() 是 Angular 裏面的一個新方法,和 $q.all() 相似,可是它只會返回第一個處理完成的 Promise 給你。假定 API 調用 1 和 API 調用 2 同時執行,而 API 調用 2 在 API 調用 1 以前處理完成,那麼你就只會獲得 API 調用 2 的返回對象。換句話說,最快(處理完成)的 Promise 會贏得返回對象的機會:

let promiseOne = $http.get('/api/todos');
let promiseTwo = $http.get('/api/comments');

// Promise 數組
$q.race([promiseOne, promiseTwo]).then(data => {
  console.log('Fastest wins, who will it be?...', data);
});

// Promise 對象哈希
// 這是 ES2015 對 { promiseOne: promiseOne, promiseTwo: promiseTwo } 的簡寫
$q.race({
    promiseOne,
    promiseTwo
  }).then(data => {
  console.log('Fastest wins, who will it be?...', data);
});複製代碼

結論

使用 $q 對非 Promise 的對象或回調構造 Promise,利用 $q.all()$q.race() 處理已存在的 Promise。

還想看更多內容,$q 文檔送上。

本文同步於個人我的博客,歡迎你們討論指正。

相關文章
相關標籤/搜索