在上节课程我们讲到因为 JavaScript 是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步和异步。

“同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;”异步模式”则完全不同,每一个任务有一个或多个回调函数,后一个任务则是不等前一个任务结束就执行,等同步任务执行完毕再去执行回调函数,所以程序的执行顺序与任务的排列顺序是不一致的。

“异步模式”非常重要,在浏览器端,耗时很长的操作像 I/O (输入/输出)网络请求都是异步操作,避免浏览器失去响应。异步最早的解决方案是回调与事件。

怎么理解呢?

JavaScript 诞生于浏览器内部,一开始的主要工作是响应用户的操作,例如onClick、onMouseOver、onChange、onSubmit等。

但是你并不知道用户何时单击按钮。 因此,为点击事件定义了一个事件处理程序。 该事件处理程序会接受一个函数,该函数会在该事件被触发时被调用:

  1. document.getElementById('button').addEventListener('click', function(){
  2. //被点击
  3. })

这就是所谓的回调。回调是一个简单的函数,会作为值被传给另一个函数,并且仅在事件发生时才被执行。

对于很多刚刚接触JavaScript的小伙伴来说你所接触的第一个异步任务就是事件。事件函数的执行不取决于代码的顺序,而取决于某个事件是否发生。

这样的写法是不是挺舒服的让很多新手玩家根本感受不到这里原来还藏着一个异步任务。
那是因为浏览器环境通过提供处理事件的API,让我们可以用同步的方式去输写代码,这样的一种编程模型。

回调无处不在,不仅在 DOM 事件中。一个常见的示例是使用定时器:

  1. setTimeout(() => {
  2. // 2 秒之后运行。
  3. }, 2000)

XHR 请求也接受回调当请求状态的改变时被调用:

  1. const xhr = new XMLHttpRequest()
  2. xhr.onreadystatechange = () => {
  3. if (xhr.readyState === 4) {
  4. xhr.status === 200 ? console.log(xhr.responseText) : console.error('出错')
  5. }
  6. }
  7. xhr.open('GET', 'http://nodejs.cn')
  8. xhr.send()

回调的问题

回调适用于简单的场景!
但是,每个回调都可以添加嵌套的层级,并且当有很多回调时,代码就会很快变得非常复杂:

  1. window.addEventListener('load', () => {
  2. document.getElementById('button').addEventListener('click', () => {
  3. setTimeout(() => {
  4. items.forEach(item => {
  5. //你的代码在这里。
  6. })
  7. }, 2000)
  8. })
  9. })

比如这种:

  1. fs.readFile(A, 'utf-8', function(err, data) {
  2. fs.readFile(B, 'utf-8', function(err, data) {
  3. fs.readFile(C, 'utf-8', function(err, data) {
  4. fs.readFile(D, 'utf-8', function(err, data) {
  5. //....
  6. });
  7. });
  8. });
  9. });

这只是一个简单的 4 个层级的代码,就已经让我们感觉代码写的受不了了。如果还有更多层级的嵌套,那代码阅读跟维护的成本将是巨大的。

所以从社区到官方ECMA组织都想了很多的办法来解决这种回调中嵌套回调的写法。不管这几年尝试去改变回调嵌套的写法 API 怎么改变请记住一个终极目标:”像同步一样去编写异步代码“。

最早在jQuery的社区里面流行了一种叫deferred (延迟函数)的解决方案,他的解决思路是这样的。 我给你提供一个deferred 对象,这个对象有三把钥匙 “resolve” “reject” “pendding”,每把钥匙对应有一个容器。 它把异步操作分了三种情况”成功” “失败” “进行中” ,当异步操作如HTTP请求 文件读取失败之后给deferred 对象绑定”reject”状态 通过”reject” 这把钥匙 找到它所管理的容器。 这个容器里存储的是一个或者多个的回调函数,依次调用这些处理函数。
从回调函数 到 Deferred 到 Promise 异步方案演进 - 图1
deferred对象的一大好处,就是它允许你自由添加多个回调函数。

  1.   $.ajax("test.html")
  2.   .done(function(){ alert("哈哈,成功了!");} )
  3.   .fail(function(){ alert("出错啦!"); } )
  4.   .done(function(){ alert("第二个回调函数!");} );

done调用所传的函数,就是往resolve状态所管理的容器中添加回调函数。fail调用所传的函数,就是往reject状态所管理的容器中添加回调函数。

deferred对象的另一大好处,就是它允许你为多个事件指定一个回调函数,这是传统写法做不到的。

  1.   $.when($.ajax("test1.html"), $.ajax("test2.html"))
  2.   .done(function(){ alert("哈哈,成功了!"); })
  3.   .fail(function(){ alert("出错啦!"); });

这段代码的意思是,先执行两个操作$.ajax(“test1.html”)和$.ajax(“test2.html”),如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。

deferred 对象确实带给我们很多便捷但是deferred 对象是重度绑定jQuery的,也就是说你想使用这个对象带来的异步编程的便捷性必须先引用jQuery, 这明显不是一种好的解决问题的方式。

所以ES6借鉴了 deferred的思想将其写进了语言标准,统一了用法,原生提供了Promise对象。 且宣称 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

一起来看下promise吧!
所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

romise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。

  1. const promise = new Promise(function(resolve, reject) {
  2. // ... some code
  3. if (/* 异步操作成功 */){
  4. resolve(value);
  5. } else {
  6. reject(error);
  7. }
  8. });

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

  1. promise.then(function(value) {
  2. // success
  3. }, function(error) {
  4. // failure
  5. });

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

下面是一个Promise对象的简单例子。

  1. $.getJSON("/post/1.json", function(post) {
  2. $.getJSON("post.comments", function(comments) {
  3. console.log("resolved: ", comments);
  4. })
  5. });

用Promise 改写:

  1. getJSON("/post/1.json").then(function(post) {
  2. return getJSON(post.commentURL);
  3. }).then(function(comments) {
  4. console.log("resolved: ", comments);
  5. },function (err){
  6. console.log("rejected: ", err);
  7. });

在异步操作需求复杂的情况下 Promise 会提供更加良好的代码组织管理方式。

第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。

讲到这里大家感觉怪怪的没有,虽然 Promise 给我们带来了在应对复杂异步操作下更好的代码组织管理方式, 但并没有降低代码的复杂度。 异步编程的终极目标是”像同步一样去编写异步代码“, 不管Promise 接口在这么强悍还是免不了回调那套东西。 所以接下来的课程我们马上就会讲到更为强悍的 Generator Async 函数 看他们是如何处理异步操作的。