关于 Angular 里的 $q 和 Promise

2,128 阅读3分钟

什么是 Promise?Angular 里的 $q 和 Promise 到底是怎么样一种关系?在 Angular 里使用 $q 到底是怎么样一种体验?本文将为你一一解答。

原文链接:All about $q and Promises in Angular
原文作者:Todd Motto
译者:linpu.li

可能你曾经见过 $q,或许已经在使用它了,但是可能你还没发现它有一些很棒的特性,比如 $q.all()$q.race()。这篇文章将深入 ES2015 的 Promise API 以及它是如何映射到 Angular 里的 $q 。换句话说,这篇文章全部是关于 $q 的,希望你能喜欢!

什么是 Promise?

Promise 是一个特殊类型对象,我们可以直接使用或者构造新的实例来处理一个异步任务。把它称作 Promise 是因为我们被“承诺”了在未来的某个时间点会得到一个结果。比如一个 HTTP 调用会在 200ms 或者 400ms 后完成,当完成之后就会有一个 Promise 执行。

一个 Promise 有三种状态:pending、resolved 和 rejected。在 Angular 里使用 $q 可以构造一个新的 Promise,但是先让我们来看看 ES2015 里的 Promise 对象来熟悉一下怎么创建它。

ES2015 里的 Promise

这里主要的内容是 Promise 和作为参数的 resolvereject

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 结果:

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 对象一样了,所以我们可以这么写:

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 文档送上。

本文同步于我的个人博客,欢迎大家讨论指正。