阅读 249

ES6 Promise原理总结

概要

  • 学习一门技术,最好的方式就是先了解这门技术是如何诞生的,及它解决了什么问题?

  • 接下来将从一下几个方面介绍Promise

    • 异步编程的问题:代码逻辑不连续;

    • 回调地狱:嵌套了太多的回调函数;

    • Promise:消灭嵌套调用;

    • Promise:合并多个任务的错误处理;

    • Promise与微任务的关系;

  • 首先明确一下,Promise解决的是异步编码风格的问题,而不是一些其他的问题

异步编程的问题:代码逻辑不连续

  • 假设有一个请求,使用XMLHttpRequest来实现,代码如下:

    // 执行状态 function onResolve(response) { console.log(response); } function onReject(error) { console.log(error); } let xhr = new XMLHttpRequest(); xhr.ontimeout = function(e) { onReject(e); }; xhr.onerror = function(e) { onReject(e); }; xhr.onreadystatechange = function () { onResolve(xhr.response); }; // 设置请求类型,请求URL,是否同步信息 let URL = 'https://localhost:8080/getList'; xhr.open('Get', URL, true); // 设置参数 xhr.timeout = 3000; // 设置xhr请求的超时时间 xhr.responseType = "text"; // 设置响应返回的数据格式 xhr.setRequestHeader("X_TEST","time.geekbang"); // 发出请求 xhr.send(); 复制代码

    • 可见上述这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式

回调地狱:嵌套了太多的回调函数

  • 当然也可以把上述的异步代码封装成一个函数,调用的时候传入相应的请求参数、回调函数,就可以让处理流程变得线性,代码如下:

    // request,请求信息,请求头,延时值,返回类型等 // resolve, 执行成功,回调该函数 // reject  执行失败,回调该函数 function XFetch(request, resolve, reject) {     let xhr = new XMLHttpRequest();     xhr.ontimeout = function (e) { reject(e); };     xhr.onerror = function (e) { reject(e); };     xhr.onreadystatechange = function () {         if (xhr.status = 200)             resolve(xhr.response);     };     xhr.open(request.method, request.url, request.sync);     xhr.timeout = request.timeout;     xhr.responseType = request.responseType;     // 补充其他请求信息     // ...     xhr.send(); } // 调用封装的异步代码 XFetch(     { method: 'GET', url: 'https://localhost:8080/getList', sync: true },     function resolve(data) {         console.log(data);     },      function reject(e) {         console.log(e);     } ); 复制代码

  • 封装异步代码在一些简单逻辑下可以满足需求,但如果逻辑复杂一点,会产生嵌套很多回调函数,从而陷入了回调地狱,代码如下:

    XFetch(     { method: 'GET', url: 'https://localhost:8080/getList', sync: true },     function resolve(response) {       console.log(response);       XFetch(           { method: 'GET', url: 'https://localhost:8080/getList1', sync: true },           function resolve(response) {               console.log(response);               XFetch(                   { method: 'GET', url: 'https://localhost:8080/getList2', sync: true },                   function resolve(response) {                       console.log(response);                   },                   function reject(e) {                       console.log(e);                   })           },           function reject(e) {               console.log(e);           })     },      function reject(e) {       console.log(e);     } ); 复制代码

  • 上述代码看上去很乱,归根结底有两点原因:

    • 嵌套调用:下一个任务依赖上一个任务的请求结果,并在上一个任务的回调函数内部执行新的业务逻辑,当嵌套层次多了之后,代码的可读性就变得非常差;

    • 任务的不确定性:执行每个任务都有两种可能的结果(成功或者失败),所以对每个任务都要进行一次额外的错误处理的方式,其明显增加了代码的混乱程度;

  • 原因分析出来后,那么就需要解决这两个问题:

    • 消灭嵌套调用

    • 合并多个任务的错误处理

  • ES6引入Promise,就是为了解决上述这两个问题,下面具体介绍;

Promise:消灭嵌套调用

Promise主要通过下面两步解决嵌套回调问题:

  • Promise实现了回调函数的延时绑定

    // 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject){     resolve(100); } let promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value){     console.log(value); } promise1.then(onResolve); 复制代码

    • 回调函数的延时绑定在代码上体现就是先创建Promise对象promise1

    • 通过Promise的构造函数executor来执行业务逻辑;

    • 创建好Promise对象promise1之后,再使用promise1.then()来设置回调函数;

    • 如上所示:

    • 总之,Promise实现回调函数的延时绑定,能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数

  • Promise将回调函数onResolve的返回值穿透到最外层

    // 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject) {     resolve(100); } const promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value) {     console.log(value);     function executor2(resolve, reject) {         resolve(value + 1);     }     return new Promise(executor2); } // promise2为内部返回值穿透到了最外层 const promise2 = promise1.then(onResolve); promise2.then((value) => {     console.log(value); }); 复制代码

    • 如上所示,onResolve函数内部创建好的Promise对象返回到最外层,这样就可以摆脱嵌套循环了

Promise:合并多个任务的错误处理

function executor(resolve, reject) {     let rand = Math.random();     console.log(1);     console.log(rand);     if (rand > 0.5)         resolve();     else         reject(); } var p0 = new Promise(executor); var p1 = p0.then((value) => {     console.log("succeed-1");     return new Promise(executor); }); var p3 = p1.then((value) => {     console.log("succeed-2");     return new Promise(executor); }); var p4 = p3.then((value) => {     console.log("succeed-3");     return new Promise(executor); }); p4.catch((error) => {     console.log("error"); }) console.log(2); 复制代码

  • 上述代码,链式调用了四个Promise对象:p0~p4无论哪个对象里面抛出异常,都可以通过最后一个对象p4.catch来捕获异常

  • 通过这种方式可以将所有Promise对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题;

  • 之所以可以使用最后一个对象来捕获所有异常,是因为Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject函数处理或catch语句捕获为止

  • 具备了这样“冒泡”的特性后,就不需要在每个Promise对象中单独捕获异常了;

Promise与微任务的关系

function executor(resolve, reject) {     resolve(100) } let demo = new Promise(executor) function onResolve(value){     console.log(value) } demo.then(onResolve) 复制代码

  • 如上代码:

    • 所以可以推测,resolve函数内部调用了通过demo.then设置的onResolve函数;

    • 首先执行new Promise时,Promise的构造函数会被执行;

    • 接下来,Promise的构造函数会调用Promise的参数executor函数;

    • 然后在executor中执行了resolve

    • 执行resolve函数,会触发demo.then设置的回调函数onResolve

  • 注:由于Promise采用了回调函数延迟绑定技术,所以在执行resolve函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行

  • 为了方便理解,这里实现了一个简单的Promise对象:

    function Bromise(executor) {     var onResolve_ = null;     var onReject_ = null;      //模拟实现resolve和then,暂不支持rejcet     this.then = function (onResolve, onReject) {         onResolve_ = onResolve;     };     function resolve(value) {           //setTimeout(()=>{             onResolve_(value);            // },0)     }     executor(resolve, null); } 复制代码

  • 调用上述定义的Bromise对象:

    function executor(resolve, reject) {     resolve(100); } // 调用Bromsie let demo = new Bromise(executor); function onResolve(value) {     console.log(value); } demo.then(onResolve); 复制代码

    • 这时会报错,因为在执行executor函数的时候,还没有通过demo.then()设置回调函数,Bromise中的onResolve_还为空,所以就报错了;

  • 这时就需要改造Bromise中的resolve方法,让resolve延迟调用onResolve_

    • 要让resolve中的onResolve_ 函数延后执行,可以在resolve函数里面加上一个定时器,让其延时执行onResolve_函数,代码如下:

      function Bromise(executor) {     var onResolve_ = null;     var onReject_ = null;     // 模拟实现resolve和then,暂不支持rejcet     this.then = function (onResolve, onReject) {         onResolve_ = onResolve;     };     function resolve(value) {           // 使用setTimeout定时器来延时`onResolve_`函数的执行           setTimeout(() => {             onResolve_(value);           }, 0);     }     executor(resolve, null); } 复制代码

    • 上面采用了定时器来推迟onResolve的执行,不过使用定时器的效率并不是太高;

  • 所以Promise又把这个定时器改造成了微任务了,这样既可以让onResolve_延时被调用,又提升了代码的执行效率,这就是Promise中使用微任务的原因


作者:YuYu_Fish
链接:https://juejin.cn/post/7018845095239614477


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐