Promise 的链式调用与停止

Abstract

本文主要讲的是如何实现 Promise 的链式调用。也就是 promise().then().then().catch() 的形式,而后讨论如何在某一个 then() 里面停止 Promise。node

在程序中,只要返回了一个 promise 对象,若是 promise 对象不是 Rejected 或 Fulfilled 状态,then 方法就会继续调用。利用这个特性,能够处理多个异步逻辑。但有时候某个 then 方法的执行结果可能会决定是否须要执行下一个 then,这个时候就需停止 promise,主要思想就是使用 reject 来停止 promise 的 then 继续执行。git

“停止”这个词不知道用得是否准确。这里可能仍是 break 的含义更精确,跳出本次 promise,不继续执行后面的 then 方法。但 promise 依旧会继续执行。github

Can I use promises

当前浏览器对 Promise 的支持状况见下图:数据库

http://caniuse.com/#search=promisejson

caniusepromise

Promise

先简单复习一下 Promise。Promise 其实很简单,就是一个处理异步的方法。通常能够经过 new 方法来调用 Promise 的构造器实例化一个 promise 对象:数组

var promise = new Promise((resolve, reject) => {
    // 异步处理
    // 处理结束后,调用 resolve 或 reject
    //      成功时就调用 resolve
    //      失败时就调用 reject
});

new Promise 实例化的 promise 对象有如下三个状态:promise

  • "has-resolution" - Fulfilled。resolve(成功)时,此时会调用 onFulfilled浏览器

  • "has-rejection" - Rejected。reject(失败)时,此时会调用 onRejected网络

  • "unresolved" - Pending。既不是resolve也不是reject的状态,也就是promise对象刚被建立后的初始化状态等session

关于上面这三种状态的读法,其中左侧为在 ES6 Promises 规范中定义的术语, 而右侧则是在 Promises/A+ 中描述状态的术语。基本上状态在代码中是不会涉及到的,因此名称也无需太在乎。

promise state

Promise Chain

先来假设一个业务需求:在系统中使用教务系统帐号进行登陆。首先用户在登陆页面输入用户名(教务系统帐号)和密码(教务系统密码);而后判断数据库中是否存在该用户;若是不存在则使用用户名和密码模拟登陆教务系统,若是模拟登陆成功,则存储用户名和密码,并返回登陆成功。

听起来就有点复杂对不对?因而画了个流程图来解释整个业务逻辑:

flow char

上图只是一个简化版本,好比密码加密、session设置等没有表现出来,你们知道就好。图中 (1)(2)(3) 三个地方就是会进行异步处理的地方,通常数据库操做、网络请求都是异步的。

若是用传统的回调函数 callback 来处理上面的逻辑,嵌套的层级就会比较深,上面的业务由于有三个异步操做因此有三层回调,代码大概会是下面的样子:

// 根据 name 查询用户信息
findUserByName(name, function(err, userinfo) {
  if (err) {
    return res.json({
      code: 1000,
      message: '查询用户信息,数据库操做数出现异常',
    });
  }


  if (userinfo.length > 0) {
  // 用户存在
  if (userinfo[0].pwd === pwd)
    // 密码正确
    return res.json({
      code: 0,
      message: '登陆成功',
    });
  }

  // 数据库中不存在该用户,模拟登陆教务系统
  loginEducationSystem(name, pwd, function(err, result) {
    if (err) {
      return res.json({
        code: 1001,
        message: '模拟登陆教务系统出现异常',
      });
    }

    // 约定正确状况下,code 为 0
    if (result.code !== 0) {
      return res.json({
        code: 1002,
        message: '模拟登陆教务系统失败,多是用户名或密码错误',
      });
    }

    // 模拟登陆成功,将用户名密码存入数据库
    saveUserToDB(name, pwd, function(err, result) {
      if (err) {
        return res.json({
          code: 1003,
          message: '将用户名密码存入数据库出现异常',
        });
      }
      if (result.code !== 0) {
        return res.json({
          code: 1004,
          message: '将用户名密码存入数据库出现异常',
        });
      }

      return res.json({
        code: 0,
        message: '登陆成功!',
      });
    });
  });
});

上面的代码可能存在的不优雅之处:

  • 随着业务逻辑变负责,回调层级会愈来愈深

  • 代码耦合度比较高,不易修改

  • 每一步操做都须要手动进行异常处理,比较麻烦

接下来再用 promise 实现此处的业务需求。使用 promise 编码以前,能够先思考两个问题。

一是如何链式调用,二是如何停止链式调用。

How to Use Promise Chain

业务中有三个须要异步处理的功能,因此会分别实例化三个 promise 对象,而后对 promise 进行链式调用。那么,如何进行链式调用?

其实也很简单,直接在 promise 的 then 方法里面返回另外一个 promise 便可。例如:

function start() {
  return new Promise((resolve, reject) => {
    resolve('start');
  });
}

start()
  .then(data => {
    // promise start
    console.log('result of start: ', data);
    return Promise.resolve(1); // p1
  })
  .then(data => {
    // promise p1
    console.log('result of p1: ', data);
    return Promise.reject(2); // p2
  })
  .then(data => {
    // promise p2
    console.log('result of p2: ', data);
    return Promise.resolve(3); // p3
  })
  .catch(ex => {
    // promise p3
    console.log('ex: ', ex);
    return Promise.resolve(4); // p4
  })
  .then(data => {
    // promise p4
    console.log('result of p4: ', data);
  });

上面的代码最终会输出:

result of start:  start
result of p1:  1
ex:  2
result of p4:  4

代码的执行逻辑如图:

promise chain

从图中能够看出来,代码的执行逻辑是 promise start --> promise p1 --> promise p3 --> promise p4。因此结合输出结果和执行逻辑图,总结出如下几点:

  • promise 的 then 方法里面能够继续返回一个新的 promise 对象

  • 下一个 then 方法的参数是上一个 promise 对象的 resolve 参数

  • catch 方法的参数是其以前某个 promise 对象的 rejecte 参数

  • 一旦某个 then 方法里面的 promise 状态改变为了 rejected,则promise 方法连会跳事后面的 then 直接执行 catch

  • catch 方法里面依旧能够返回一个新的 promise 对象

How to Break Promise Chain

接下来就该讨论如何停止 promise 方法链了。

经过上面的例子,咱们能够知道 promise 的状态改变为 rejected 后,promise 就会跳事后面的 then 方法。

也就是,某个 then 里面发生异常后,就会跳过 then 方法,直接执行 catch。

因此,当在构造的 promise 方法链中,若是在某个 then 后面,不须要再执行 then 方法了,就能够把它看成一个异常来处理,返回一个异常信息给 catch,其参数可自定义,好比该异常的参数信息为 { notRealPromiseException: true},而后在 catch 里面判断一下 notRealPromiseException 是否为 true,若是为 true,就说明不是程序出现异常,而是在正常逻辑里面停止 then 方法的执行。

代码大概就这样:

start()
  .then(data => {
    // promise start
    console.log('result of start: ', data);
    return Promise.resolve(1); // p1
    )
  .then(data => {
    // promise p1
    console.log('result of p1: ', data);
    return Promise.reject({
      notRealPromiseException: true,
    }); // p2
  })
  .then(data => {
    // promise p2
    console.log('result of p2: ', data);
    return Promise.resolve(3); // p3
  })
  .catch(ex => {
    console.log('ex: ', ex);
    if (ex.notRealPromiseException) {
      // 一切正常,只是经过 catch 方法来停止 promise chain
      // 也就是停止 promise p2 的执行
      return true;
    }
    // 真正发生异常
    return false;
  });

这样的作法可能不符合 catch 的语义。不过从某种意义上来讲,promise 方法链没有继续执行,也能够算是一种“异常”。

Refactor Callback with Promise

讲了那么多道理,如今就改来使用 promise 重构以前用回调函数写的异步逻辑了。

// 据 name 查询用户信息
const findUserByName = (name, pwd) => {
  return new Promise((resolve, reject) => {
    // 数据库查询操做
    if (dbError) {
      // 数据库查询出错,将 promise 设置为 rejected
      reject({
        code: 1000,
        message: '查询用户信息,数据库操做数出现异常',
      });
    }
    // 将查询结果赋给 userinfo 变量
    if (userinfo.length === 0) {
      // 数据库中不存在该用户
      resolve();
    }
    // 数据库存在该用户,判断密码是否正确
    if (pwd === userinfo[0].pwd) {
      // 密码正确,停止 promise 执行
      reject({
        notRealPromiseException: true,
        data: {
          code: 0,
          message: '密码正确,登陆成功',
        }
      });
    }
    // 密码不正确,登陆失败,将 Promise 设置为 Rejected 状态
    reject({
      code: 1001,
      message: '密码不正确,登陆失败',
    });
  });
};


// 模拟登陆教务系统
const loginEducationSystem = (name, pwd) => {
  // 登陆逻辑...
  // 登陆成功
  resolve();
  // 登陆失败
  reject({
    code: 1002,
    message: '模拟登陆教务系统失败',
  });
};


// 将用户名密码存入数据库
const saveUserToDB(name, pwd) => {
  // 数据库存储操做
  if (dbError) {
    // 数据库存储出错,将 promise 设置为 rejected
    reject({
      code: 1004,
      message: '数据库存储出错,将出现异常',
    });
  }
  // 数据库存储操做成功
  resolve();
};


findUserByName(name)
.then(() => {
  return loginEducationSystem(name, pwd);
})
.then(() => {
  return saveUserToDB(name, pwd);
})
.catch(e => {
  // 判断异常出现缘由
  if (e.notRealPromiseException) {
    // 正常停止 promise 而故意设置的异常
    return res.json(e.data);
  }
  // 出现错误或异常
  return res.json(e);
});

在上面的代码中,实例化了三个 promise 对象,分别实现业务需求中的三个功能。而后经过 promise 方法链来调用。相比用回调函数而言,代码结构更加清晰,也更易读易懂耦合度更低更易扩展了。

Promise.all && Promise.race

仔细观察能够发现,在上面的 promise 代码中,loginEducationSystemsaveUserToDB 两个方法执行有前后顺序要求,但没有数据传递。

其实 promise 方法链更好用的一点是,当下一个操做依赖于上一个操做的结果的时候,能够很方便地经过 then 方法的参数来传递数据。前面页提到过,下一个 then 方法的参数就是上一个 then 方法里面 resolve 的参数,因此固然就能够把上一个 then 方法的执行结果做为参数传递给下一个 then 方法了。

还有些时候,可能 then 方法的执行顺序也没有太多要求,只须要 promise 方法链中的两个或多个 promise 所有都执行正确。这时,若是依旧一个一个去写 then 可能就比较麻烦,好比:

function p1() {
  return new Promise((resolve) => {
    console.log(1);
    resolve();
  });
}

function p2() {
  return new Promise((resolve) => {
    console.log(2);
    resolve();
  });
}

function p3() {
  return new Promise((resolve) => {
    console.log(3);
    resolve();
  });
}

如今只须要 p1 p2 p3 这三个 promise 都执行,而且 promise 最终状态都是 Fulfilled,那么若是仍是使用方法链,这是这样调用:

p1()
.then(() => {
  return p2();
})
.then(() => {
  return p3();
})
.then(() => {
  console.log('all done');
})
.catch(e => {
  console.log('e: ', e);
});

// 输出结果:
// 1
// 2
// 3
// all done

代码貌似就不那么精炼了。这个时候就有了 Promise.all 这个方法。

Promise.all 接收一个 promise对象的数组做为参数,当这个数组里的全部 promise 对象所有变为 resolve 或 reject 状态的时候,它才会去调用 then 方法。

因而,调用这几个 promise 的代码就能够这样写了:

p1()
.then(() => {
  return Promise.all([
    p2(),
    p3(),
  ]);
})
.then(() => {
  console.log('all done');
})
.catch((e) => {
  console.log('e: ', e);
});

// 输出结果:
// 1
// 2
// 3
// all done

这样看起来貌似就精炼些了。

而对于 Promise.race,其参数也跟 Promise.all 同样是一个数组。只是数组中的任何一个 promise 对象若是变为 resolve 或者reject 的话,该函数就会返回,并使用这个 promise 对象的值进行 resolve 或者 reject。

这里就不举例了。

Conclusion

到目前为止,咱们就基本了解了 Promise 的用法及特色,并实现用 Promise 重构用回调函数写的异步操做。如今对 Promise 的使用,应该得心应手了。

完。


Github Issue: https://github.com/nodejh/nodejh.github.io/issues/23

相关文章
相关标签/搜索