Better

Ethan的博客,欢迎访问交流

Promise深入学习

Promise是实现异步编程的一种重要方式,在Angular的使用过程中比较简单,在这里深入学习。

Promise起源

异步编程

  • 回调函数
  • 事件监听
  • 发布订阅
  • Promise对象

回调函数问题

  • 金字塔问题
  • 剥夺了我们使用 return 和 throw 这些关键字的能力
  • 整个代码流程都是基于副作用的
  • 堆栈

简单来说就是一个函数除了会返回一个值之外,还会修改函数以外的状态如全局变量等等。实际上所有异步调用都可以视为带有副作用的行为。

Promise基础

基本API如下

function Promise(resolver) {}

Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}

Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}

新手错误

Promise版金字塔

出现的原因就是像使用回调一样使用 promises。比如有多个promise需要执行且有先后顺序,常见错误代码如下:

remotedb.allDocs().then(function(resultOfAllDocs){
    localdb.put(resultOfAllDocs).then(function(resultOfPut){
        localdb.get(resultOfPut).then(function(resultOfGet){
            localdb.put(resultOfGet).then(function(resultOfPut){
                //...
            })
        })
    })
})

正确的风格应该如下:

remotedb.allDocs(...).then(function (resultOfAllDocs) {
  return localdb.put(...);
}).then(function (resultOfPut) {
  return localdb.get(...);
}).then(function (resultOfGet) {
  return localdb.put(...);
}).catch(function (err) {
  console.log(err);
});

这种写法被称为 composing promises ,是 promises 的强大能力之一。每一个函数只会在前一个 promise 被调用并且完成回调后调用,并且这个函数会被前一个 promise 的输出调用

用了 promises 后怎么用 forEach

例子:删除所有doc后执行某操作,下面是错误代码

// I want to remove() all docs
db.allDocs({include_docs: true}).then(function (result) {
  result.rows.forEach(function (row) {
    db.remove(row.doc);  
  });
}).then(function () {
  // I naively believe all docs have been removed() now!
});

问题在于第一个函数实际上返回的是 undefined,这意味着第二个方法不会等待所有 documents 都执行 db.remove()。实际上他不会等待任何事情,并且可能会在任意数量的文档被删除后执行!

这里需要 Promise.all(),具体功能点有:

  1. Promise.all()会以一个 promises 数组为输入,并且返回一个新的 promise。这个新的 promise 会在数组中所有的 promises 都成功返回后才返回。
  2. Promise.all() 会将执行结果组成的数组返回到下一个函数。
  3. 一旦数组中的 promise 任意一个返回错误,Promise.all() 也会返回错误。
db.allDocs({include_docs: true}).then(function (result) {
  return Promise.all(result.rows.map(function (row) {
    return db.remove(row.doc);
  }));
}).then(function (arrayOfResults) {
  // All docs have really been removed() now!
});

忘记使用 .catch()

即使你坚信不会出现异常,添加一个 catch() 总归是更加谨慎的。如果你的假设最终被发现是错误的,它会让你的生活更加美好。

使用副作用调用而非返回

考虑下面代码问题

somePromise().then(function () {
  someOtherPromise();
}).then(function () {
  // Gee, I hope someOtherPromise() has resolved!
  // Spoiler alert: it hasn't.
});

这是一个一旦你理解了它,就会避免所有我提及的古怪的错误的技巧。promises 的奇妙在于给予我们以前的 return 与 throw。 每一个 promise 都会提供给你一个 then() 函数 (或是 catch(),实际上只是 then(null, ...) 的语法糖)。当我们在 then() 函数内部时:

somePromise().then(function () {
  // I'm inside a then() function!
  // 1.return 另一个 promise
  // 2.return 一个同步的值 (或者 undefined)
  // 3.throw 一个同步异常 
});
1.返回另一个 promise
getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // I got a user account!
});

注意到我是 return 第二个 promise,这个 return 非常重要。如果我没有写 returngetUserAccountById() 就会成为一个副作用,并且下一个函数将会接收到 undefined 而非 userAccount

2.返回一个同步值 (或者 undefined)

返回 undefined 通常是错误的,但是返回一个同步值实际上是将同步代码包裹为 promise 风格代码的一种非常赞的手段。

getUserByName('nolan').then(function (user) {
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];    // returning a synchronous value!
  }
  return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
});

不幸的是,有一个不便的现实是在 JavaScript 中无返回值函数在技术上是返回 undefined,这就意味着当你本意是返回某些值时,你很容易会不经意间引入副作用。

出于这个原因,我个人养成了在 then() 函数内部 永远返回或抛出 的习惯。我建议你也这样做。

3.抛出同步异常

谈到 throw,这是让 promises 更加赞的一点。比如我们希望在用户已经登出时,抛出一个同步异常。如果用户已经登出,我们的 catch() 会接收到一个同步异常,并且如果 后续的 promise 中出现异步异常,他也会接收到。如果是使用回调风格,这个错误很可能就会被吃掉,但是使用 promises,我们可以轻易的在 catch() 函数中处理它了。

进阶错误

不知道 Promise.resolve()

promises 在封装同步与异步代码时非常的有用。然而,如果你发现你经常写出下面的代码:

new Promise(function (resolve, reject) {
  resolve(someSynchronousValue);
}).then(/* ... */);

你会发现使用 Promise.resolve 会更加简洁:

Promise.resolve(someSynchronousValue).then(/* ... */);

Promise.resolve方法对于参数是有要求的,下面是三种形式:

  • Promise.resolve(value);
  • Promise.resolve(promise);
  • Promise.resolve(thenable);
  • 这三种形式都会产生一个新的Promise。
  • 第一种形式提供了自定义Promise的值的能力。
  • 第二种形式,提供了创建一个Promise的副本的能力。
  • 第三种形式,是将一个类似Promise的对象转换成一个真正的Promise对象。

new Promise()的区别看例子:

var foo = {
    then: (resolve, reject) => resolve('foo')
};
var promise=Promise.resolve(foo);
-->
var promise=new Promise(function(resovle,reject){
    foo.then(resovle,reject);
})
promise.then(function(data){
    console.log(data);//'foo'
})

catch() 与 then(null, ...) 并非完全等价

区别就是:当你使用 then(resolveHandler, rejectHandler) 这种形式时,rejectHandler 并不会捕获由 resolveHandler 引发的异常。

promises vs promises factories

当我们希望执行一个个的执行一个 promises 序列,即类似 Promise.all() 但是并非并行的执行所有 promises。代码如下:

function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    result = result.then(promise);
  });
  return result;
}

上面的代码本身没有任何问题,关键在于参数的传入,这也是为什么本节的标题是promisespromises工厂,由于依照 promises 规范,一旦一个 promise 被创建,它就被执行了。,因此传入的不能是已经创建好的Promise对象,而是一些返回Promise对象的函数。DEMO如下:

function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    result = result.then(promise);
  });
  return result;
}

function myPromiseFactory(fun){
    return function(){
        return new Promise(fun);
    }
}

var promises=[
    myPromiseFactory(function(resovle,reject){
        setTimeout(function(){
            console.log(1);
            resovle(1);
        },2000);
    }),
    myPromiseFactory(function(resovle,reject){
        setTimeout(function(){
            console.log(2);
            resovle(2);
        },1000);
    })
];

executeSequentially(promises);

如果我希望获得两个 promises 的结果怎么办

如果我们希望同时获得这两个 promises 的输出。举例来说:

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // dangit, I need the "user" object too!
});

方法如下:

  • 避免金字塔问题,将一个Promise的输入存在一个更高的作用域中的变量里,这样第二个就能访问到两个Promise的输入。
  • 抛弃成见,拥抱金字塔
    getUserByName('nolan').then(function (user) {
    return getUserAccountById(user.id).then(function (userAccount) {
      // okay, I have both the "user" and the "userAccount"
    });
    });
    
  • 一旦缩进开始成为问题,你可以通过 Javascript 开发者从远古时期就开始使用的技巧,将函数抽离到一个命名函数中:
    function onGetUserAndUserAccount(user, userAccount) {
    return doSomething(user, userAccount);
    }
    function onGetUser(user) {
    return getUserAccountById(user.id).then(function (userAccount) {
      return onGetUserAndUserAccount(user, userAccount);
    });
    }
    getUserByName('nolan')
    .then(onGetUser)
    .then(function () {
    // at this point, doSomething() is done, and we are back to indentation 0
    });
    

promises 穿透

下面的结果会是什么呢?

Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
  console.log(result);
});

如果你认为它会打印出 bar,那么你就错了。它实际上打印出来的是 foo!

发生这个的原因是如果你像 then() 传递的并非是一个函数(比如 promise),它实际上会将其解释为 then(null),这就会导致前一个 promise 的结果会穿透下面。

你可以直接传递一个 promise 到 then() 函数中,但是它并不会按照你期望的去执行。then() 是期望获取一个函数,因此你希望做的最可能是:

Promise.resolve('foo').then(function () {
  return Promise.resolve('bar');
}).then(function (result) {
  console.log(result);
});

大神指导



留言