资讯专栏INFORMATION COLUMN

Promise 的链式调用与中止

cuieney / 3485人阅读

摘要:一是如何链式调用,二是如何中止链式调用。到目前为止,我们就基本了解了的用法及特点,并实现用重构用回调函数写的异步操作。

Abstract

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

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

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

Can I use promises

当前浏览器对 Promise 的支持情况见下图:

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

Promise

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

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

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

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

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

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

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

Promise Chain

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

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

上图只是一个简化版本,比如密码加密、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 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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81048.html

相关文章

  • Promise 对象理解

    摘要:使用对象的好处在于可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。对象异步操作抛出错误,状态就会变为,就会调用方法指定的回调函数处理这个错误。 Promise 含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。 所谓 P...

    church 评论0 收藏0
  • 异步Promise及Async/Await可能最完整入门攻略

    摘要:的和我们通过的原型方法拿到我们的返回值输出我延迟了毫秒后输出的输出下列的值我延迟了毫秒后输出的。有人说,我不想耦合性这么高,想先执行函数再执行,但不想用上面那种写法,可以吗,答案是当然可以。 此文只介绍Async/Await与Promise基础知识与实际用到注意的问题,将通过很多代码实例进行说明,两个实例代码是setDelay和setDelaySecond。 tips:本文系原创转自...

    lingdududu 评论0 收藏0
  • indexedDB事务功能Promise化封装

    摘要:综上,对进行一定的封装,来简化编码操作。化的尝试对于这种带大量回调的,使用进行异步化封装是个好主意。因此包括在内的所有异步方法都会强制中止当前事务。这就决定了一个事务内部的所有操作必须是同步完成的。目前只实现了和,其他的有待下一步工作。 前言 本文是介绍我在编写indexedDB封装库中诞生的一个副产品——如何让indexedDB在支持链式调用的同时,保持对事务的支持。项目地址:htt...

    zombieda 评论0 收藏0
  • 一起来学Promise

    摘要:参数如前面所提到的,方法只是方法的一个语法糖,原因就在于方法的参数为实际上是两个回调函数,分别用于处理调用它的对象的和状态,而方法就等价于状态处理函数。对象状态传递和改变的方法利用回调的返回值,可以控制某个操作后方法返回的对象及其状态。 注意,本文主要针对ES6标准实现的Promise语法进行阐述,实例代码也都使用ES6语法,快速入门ES6请参见ECMAScript 6 扫盲。 一分钟...

    liaoyg8023 评论0 收藏0
  • 深入学习Promise调用

    摘要:前言使用中,链式的调用对于控制异步执行很重要。的链式调用是支持链式调用的,但是它是不同于上面的链式。是调用方法返回自身,但是是调用方法后返回一个新的。的运行机制请参考的运行机制值穿透由于通过没有成功添加回调函数,发生了值穿透。 前言 使用Promise中,链式的调用对于控制异步执行很重要。 链式调用 在jQuery的使用中,我们常常使用下面的代码 $(#app).show().css(...

    tianren124 评论0 收藏0

发表评论

0条评论

cuieney

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<