Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise初入门径 #8

Open
CruxF opened this issue Sep 2, 2018 · 0 comments
Open

Promise初入门径 #8

CruxF opened this issue Sep 2, 2018 · 0 comments

Comments

@CruxF
Copy link
Owner

CruxF commented Sep 2, 2018

前言

这篇博文是根据慕课网教程整理而来,内容几乎都会是讲师的原话,外带一些自己的理解。

Promise是什么

这个英语单词翻译成中文意思就是:许诺;允诺;有可能。因此从字面上就可以知道它代表了即将要发生的事情,从而联想到了JavaScript中异步程序。

按照它的实际用途来看主要有以下几点

  • 用于异步计算
  • 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
  • 可以在对象之间传递和操作Promise,帮助我们处理队列

Promise产生的背景

根源是为了优化表单提交的用户体验,而开发了JavaScript这款包含大量异步操作的脚本语言。在提交表单中异步程序的表现是怎么样的呢?就是当你注册会员的时候,填写了昵称这玩意,然后再填写密码的时候,同时服务器里会检测这个昵称是否已经被注册从而做出一些回应,而不用等你全部信息填写好点击提交才告诉你昵称已经存在。

借由异步的这一个特点,可以想到:异步操作能够避免界面冻结!异步的本质用大白话说就是:将耗时很长的A交付的工作交给系统之后,就去继续做B交付的工作。等到系统完成前面的工作之后,再通过回调或者事件,继续做A交付的剩下的工作。

从观察者的角度看起来,AB工作的完成顺序,和交付它们的时间顺序无关,所以叫“异步”。

咳咳,说重点,以下才是Promise诞生的原因

  • 解决因为异步操作所带来的回调地狱,从而导致维护性差,下面请看回调代码
a(function (resultsFromA) {
  b(resultsFromA, function (resultsFromB) {
    c(resultsFromB, function (resultsFromC) {
      d(resultsFromC, function (resultsFromD) {
        e(resultsFromD, function (resultsFromE) {
          f(resultsFromE, function (resultsFromF) {
            console.log(resultsFromF);
          })
        })
      })
    })
  })
});
  • 总结就是曾经的异步操作依赖的回调函数中存在着“嵌套层次深,难以维护”、“无法正常使用return和throw”、“无法正常检索堆栈信息”和“多个回调之间难以建立联系”这四个主要问题需要被解决,于是Promise横空出世。
  • 最后一点:到底啥是回调函数啊??!

Promise的概念和优点

【优点】

  • Promise是一个代理对象,它和原先要进行的操作并无关系
  • Promise通过引入一个回调,避免了更多的回调

【状态】

  • pending:待定,称为初始状态
  • fulfilled:实现,称为操作成功状态
  • rejected:被否决,称为操作失败状态
  • 当Promise状态发生改变的时候,就会触发.then()里的响应函数来处理后续步骤
  • Promise状态已经改变,就不会再变

Promise的基本语法

new Promise(
    /* 执行器 executor */
    function (resolve, reject) {
      // 一段耗时很长的异步操作
      resolve(); // 数据处理完成
      reject(); // 数据处理出错
    }
 ).then(function A() {
    // 成功,下一步
  }, function B() {
    // 失败,做相应处理
  });

异步操作的常见方法

首先看课程里提供的方法

// 事件侦听与响应
document.getElementById('start').addEventListener('click', start, false);
function start() {
  // 响应事件,进行相应的操作
}

// jQuery 用 `.on()` 也是事件侦听
$('#start').on('click', start);

// 回调,比较常见的有ajax
$.ajax('http://baidu.com', {
  success: function (res) {
    // 这里就是回调函数了
  }
});

// 或者在页面加载完毕后回调
$(function () {
  // 这里也是回调函数
});

以上是课程稍微提到的方法,下面请看阮一峰老师的进一步说明,面试的时候可以使劲说啦,传送门在此。

Promise一个简单的例子

console.log('here we go');
new Promise(resolve => {
    setTimeout(() => {
      resolve('hello');
      console.log(123);
    }, 2000);
  })
  .then(name => {
    console.log(name + ' world');
  });

以上代码和课程稍微有些不同,目的是和定时器做一些对比,以此发现一点什么。

console.log('here we go');
setTimeout(() => {
  callback("hello");
  console.log(123);
}, 2000)

function callback(name) {
  console.log(name + ' world');
}

通过以上两段代码的运行结果比较,可以浅显的得出:resolve()状态引发的then()是异步的,更多的我暂时就不知道啦。

Promise两步执行的范例

console.log('here we go');
new Promise(resolve => {
    setTimeout(() => {
      resolve('hello');
    }, 2000);
  })
  .then(value => {
    console.log(value);
    return new Promise(resolve => {
      setTimeout(() => {
        resolve('world');
      }, 2000);
    });
  })
  .then(value => {
    console.log(value + ' world');
  });

这个范例主要是简单的演示了Promise如何解决回调地狱这个让人头大的问题。

对已经完成的Promise执行then()

console.log('start');
let promise = new Promise(resolve => {
  setTimeout(() => {
    console.log('the promise fulfilled');
    resolve('hello, world');
  }, 1000);
});

setTimeout(() => {
  promise.then(value => {
    console.log(value);
  });
}, 3000);

讲师的原话:这段代码展示了Promise作为队列这个重要的特性,就是说我们在任何一个地方生成了一个Promise对象,都可以把它当做成一个变量传递到其他地方执行。不管Promise前面的状态到底有没有完成,队列都会按照固定的顺序去执行。

then()不返回Promise

console.log('here we go');
new Promise(resolve => {
    setTimeout(() => {
      resolve('hello');
    }, 2000);
  })
  .then(value => {
    console.log(value);
    console.log('everyone');
    (function () {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('Mr.Laurence');
          resolve('Merry Xmas');
        }, 2000);
      });
    }());
    return false;
  })
  .then(value => {
    console.log(value + ' world');
  });

我对以上代码的理解是这样的:最后一个then()方法里的value值代表的是上一个then()里的返回值,当没有return的时候,默认返回值为undefined。而resolve()里的数据为什么没被调用呢?因为上一个then()方法里return的是false而不是Promise实例。

要想调用resolve()里的数据,只要这么写就可以了

console.log('here we go');
new Promise(resolve => {
    setTimeout(() => {
      resolve('hello');
    }, 2000);
  })
  .then(value => {
    console.log(value);
    console.log('everyone');
    (function () {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('Mr.Laurence');
          resolve('Merry Xmas');
        }, 2000);
      });
    }()).then(value => {
      console.log(value + ' world');
    });
  })

then()解析

  • then()接受两个函数作为参数,分别代表fulfilled和rejected
  • then()返回一个新的Promise实例,所以它可以链式调用
  • 当前面的Promise状态改变时,then()根据其最终状态,选择特定的状态响应函数执行
  • 状态响应函数可以返回新的Promise或其他值
  • 如果返回新的Promise,那么下一级then()会在新的Promise状态改变之后执行
  • 如果返回其他任何值,则会立刻执行下一级then()

then()的嵌套

then()里面有then()的情况:因为then()返回的还是Promise实例,故会等里面的then()执行完,再执行外面的,因此对于我们来说,此时最好将其展开,会更好的进行阅读。以下是then嵌套的代码

console.log('start');
new Promise(resolve => {
    console.log('Step 1');
    setTimeout(() => {
      resolve(100);
    }, 1000);
  })
  .then(value => {
    return new Promise(resolve => {
        console.log('Step 1-1');
        setTimeout(() => {
          resolve(110);
        }, 1000);
      })
      .then(value => {
        console.log('Step 1-2');
        return value;
      })
      .then(value => {
        console.log('Step 1-3');
        return value;
      });
  })
  .then(value => {
    console.log(value);
    console.log('Step 2');
  });

解套后的代码为:

console.log('start');
new Promise(resolve => {
    console.log('Step 1');
    setTimeout(() => {
      resolve(100);
    }, 1000);
  })
  .then(value => {
    return new Promise(resolve => {
      console.log('Step 1-1');
      setTimeout(() => {
        resolve(110);
      }, 1000);
    })
  })
  .then(value => {
    console.log('Step 1-2');
    return value;
  })
  .then(value => {
    console.log('Step 1-3');
    return value;
  })
  .then(value => {
    console.log(value);
    console.log('Step 2');
  });

两段代码的执行结果一致(话说此时你们清楚的知道结果是啥吗)。

Promise小测试

看以下代码进行分析四种Promise的区别是什么?是什么原因导致了不同的执行流程?

// 问题一
doSomething()
  .then(function () {
    return doSomethingElse();
  })
  .then(finalHandler);
//执行流程为doSomething ==> doSomethingElse(undefined) ==> finalHandler(resultDoSomethingELlse)

// 问题二
doSomething()
  .then(function () {
    doSomethingElse();
  })
  .then(finalHandler);
//执行流程为doSomething ==> doSomethingElse(undefined) ==> finalHandler(undefined)
//注意:doSomethingElse(undefined)和finalHandler(undefined)同时执行

// 问题三
doSomething()
  .then(doSomethingElse())
  .then(finalHandler);
//执行流程为doSomething ==> doSomethingElse(undefined) ==> finalHandler(resultOfDoSomething)
//注意:doSomethingElse(undefined)和doSomething()同时执行


// 问题四
doSomething()
  .then(doSomethingElse)
  .then(finalHandler);
//执行流程为doSomething ==> doSomethingElse(resultOfDoSomething) ==> finalHandler(resultOfDoSomethingElse)

Promise错误处理

因为在这一块,讲师貌似犯了些小错误,很多人反应很强烈,至于这错误到底是不是错误我也不太懂,但是不能把有异议的内容也写进来吧,于是我在网上找了篇自己能理解的Promise错误处理,贴上来给大家看看。

谈到Promise错误处理,就要把reject拿出来晾一晾了。reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调(严格来说这不算是错误处理吧。。。),看下面的代码。

function getNumber() {
  var p = new Promise(function(resolve, reject) {
    //做一些异步操作
    setTimeout(function() {
      var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
      if(num <= 5) {
        resolve(num);
      } else {
        reject('数字太大了');
      }
    }, 2000);
  });
  return p;
}
getNumber().then(function(data) {
  console.log('resolved');
  console.log(data);
}, function(reason) {
  console.log('rejected');
  console.log(reason);
});

getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。

运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到“成功”和“失败”的两种结果。

另一种处理错误和异常的方法:catch。 其实它和上面then的第二个参数一样,用来指定reject的回调,用法是这样的:

function getNumber() {
  var p = new Promise(function(resolve, reject) {
    //做一些异步操作
    setTimeout(function() {
      var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
      if(num <= 5) {
        resolve(num);
      } else {
        reject('数字太大了');
      }
    }, 2000);
  });
  return p;
}
getNumber().then(function(data) {
  console.log('resolved');
  console.log(data);
}).catch(function(reason) {
  console.log('rejected');
  console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中,请看下面的代码,然后分别代入自行测试一下

// 测试代码1
getNumber().then(function(data) {
  console.log(name());
  console.log('resolved');
  console.log(data);
}, function(reason, data) {
  console.log('rejected');
  console.log(reason);
});
// 测试代码2
getNumber().then(function(data) {
  console.log(name());
  console.log('resolved');
  console.log(data);
}).catch(function(reason) {
  console.log('rejected');
  console.log(reason);
});

在resolve的回调中,我们console.log(name());而name()这个函数是没有被定义的。如果我们不用Promise中的 catch,代码运行到这里就直接在控制台报错了,不往下运行,但是使用catch就不同了。

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

Promise.all()解析

Promise.all()具有批量执行的特点,用于将多个Promise实例,包装成一个新的Promise实例,返回的就是普通Promise。

它接受一个数组作为参数,数组里可以是Promise对象,也可以是别的值,只有Promise会等待状态的改变。

当所有子Promise都完成,那么返回新的Promise才认为是完成了,返回值是全部值的数组;有任何一个失败,则新的Promise就认为失败了,返回值是第一个失败的子Promise的结果。下面看代码:

console.log('here we go');
Promise.all([1, 2, 3]).then(all => {
  console.log('1:', all);
  return Promise.all([function() {
    console.log('ooxx');
  }, 'xxoo', false]);
}).then(all => {
  console.log('2:', all);
  let p1 = new Promise(resolve => {
    setTimeout(() => {
      resolve('I\'m P1');
    }, 1500);
  });
  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('I\'m P2');
    }, 1000);
  });
  let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('I\'m P3');
    }, 3000);
  });
  return Promise.all([p1, p2, p3]);
}).then(all => {
  console.log('all', all);
}).catch(err => {
  console.log('Catch:', err);
});

真是让人头大的代码,建议各位还是先来看看这篇吧——大白话讲解Promise

Promise实现队列

有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行,用代码解释就是如下这样的:

let promise = doSomething();
promise = promise.then(doSomethingElse);
promise = promise.then(doSomethingElse2);
promise = promise.then(doSomethingElse3);
.........

实现队列方式一:使用forEach()

function queue(things) {
  let promise = Promise.resolve();
  things.forEach(thing => {
   promise.then(() => {
      return new Promise(resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  });
  return promise;
}
queue(['lots', 'of', 'things', ....]);

【注意】
常见错误:没有把then()产生的新Promise实例赋给promise,没有生成队列。

实现队列方式二:使用reduce()

function queue(things) {
  return things.reduce((promise, thing) => {
    return promise.then(() => {
      return new Promise(resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  }, Promise.resolve());
}
queue(['lots', 'of', 'things', ....]);

Promise.resolve()解析

Promise.resolve()返回一个fulfilled状态的Promise实例,或原始的Promise实例,具有如下特点:

  • 参数为空,返回一个状态为fulfilled的Promise实例
  • 参数是一个跟Promise无关的值,同上,不过fulfilled响应函数会得到这个参数
  • 参数为Promise实例,则返回该实例,不做任何修改
  • 参数为thenble,则立刻执行它的then()
    看以下代码逐一分析
console.log('start');
Promise.resolve().then(() => {
  console.log('Step 1');
  return Promise.resolve('Hello');
}).then(value => {
  console.log(value, 'World');
  return Promise.resolve(new Promise(resolve => {
    setTimeout(() => {
      resolve('Good');
    }, 2000);
  }));
}).then(value => {
  console.log(value, ' evening');
  return Promise.resolve({
    then() {
      console.log(', everyone');
    }
  })
})

Promise.reject()解析

Promise.reject()除了不认thenable,其他的特点都和Promise.resolve()类似,请看如下代码:

let promise = Promise.reject('something wrong');
promise.then(() => {
  console.log('it\'s ok');
}).catch(() => {
  console.log('no, it\'s not ok');
  return Promise.reject({
    then() {
      console.log('it will be ok');
    },
    catch() {
      console.log('not yet');
    }
  });
});

Promise.race()解析

类似Promise.all(),区别在于它有任意一个完成就算完成,观察以下代码:

console.log('start');
let p1 = new Promise(resolve => {
  // 这是一个长时间的调用
  setTimeout(() => {
    resolve('I\'m P1');
  }, 10000);
});
let p2 = new Promise(resolve => {
  // 这是个稍短的调用
  setTimeout(() => {
    resolve('I\'m P2');
  }, 2000)
});
Promise.race([p1, p2]).then(value => {
  console.log(value);
});

Promise.race()常见用法是把异步操作和定时器放在一起,如果定时器先触发,就认为超时,告知用户。这里可能说的有点抽象,希望来这里看一看——大白话讲解Promise,那么很容易就能明了。

现实生活中的Promise应用

  • 把回调函数包装成Promise,使其可读性更好和返回的结果可以加入任何Promise队列
  • 把任何异步操作包装成Promise,假设有这需求:用户点击按钮,弹出确认窗体 ==> 用户确认和取消有不同的处理。那么就能编写如下代码:
// 弹出窗体
let confirm = popupManager.confirm('您确定么?');
confirm.promise.then(() => {
  // do confirm staff
}).catch(() => {
  // do cancel staff
});
// 窗体的构造函数
class Confirm {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.confirmButton.onClick = resolve;
      this.cancelButton.onClick = reject;
    })
  }
}

尾声

呃呃,IE那块我选择不鸟它了,一个连它爸爸都嫌弃的浏览器也是没救了。还有最新的异步函数async和await,不说了,困得一批,劳资下班睡觉去。

最后的最后,强烈推荐一篇反复出现的博文和那位博主,有非常值得学习的地方:大白话讲解Promise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant