ES6 —— Promise

一、介绍

阮一峰老师的 ES6 Promise 文章里,把 Promise 介绍地很详细。这里再整理一下,加深理解。

学习一个东西,得先知道它是什么。我们先在浏览器中使用 console.dir(Promise) 打印出 Promise 对象的所的属性和方法。

Promise

从打印结果可以看出,Promise 是一个构造函数,它自己本身有 allrejectresolve 等方法,原型上有 catchfinallythen 等方法。所以 new 出来的 Promise 对象也就自然拥有 catchfinallythen 这些方法。从上图中可以看到,then 方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

Promise 的中文意思是承诺,这种**“承诺将来会执行”**的对象在 JavaScript 中称为 Promise 对象。简单说就是一个容器,里面保存着某个未来才会执行的事件(通常是一个异步操作)的结果。

Promise对象有以下两个特点。

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

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

Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

—— 摘自 http://es6.ruanyifeng.com/#docs/promise

二、Promise的使用

1、创建Promise

那如何创建一个 Promise 呢,下面看一个简单的例子:

const p = new Promise(function(resolve, reject) {
  //Do some Async
  setTimeout(function() {
    console.log('执行完成');
    resolve('数据');
  }, 2000);
});

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject,这两个参数也是函数,由 JavaScript 引擎提供,不用自己实现。

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

在上面的代码中,我们执行了一个异步操作,也就是 setTimeout,2秒后,输出“执行完成”,并且调用 resolve方法。运行代码的时候我们发现,我们只是 new 了一个 Promise 对象,并没有调用它,我们传进去的函数就已经执行了。所以,我们使用 Promise 的时候一般是包在一个函数中,在需要的时候去运行这个函数 :

function runAsync() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('执行完成');
      resolve('数据');
    }, 2000);
  });
  return p;
}
runAsync();

函数会 returnPromise 对象,也就是说,执行这个函数我们得到了一个 Promise 对象。在文章开始的时候,我们知道 Promise 对象拥有 catchfinallythen 这些方法,现在我们看看怎么使用它们。继续使用上面的 runAsync 函数 :

function runAsync() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('执行完成');
      resolve('数据');
    }, 2000);
  });
  return p;
}
runAsync().then(
  function(data) {
    // success
    console.log(`成功拿到${data}`);
    //后面可以用传过来的数据做些其他操作
    // ......
  },
  function(error) {
    // failure
    console.log(error);
  }
);

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。Promise实例的状态变为 resolvedrejected,就会触发 then 方法绑定的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

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

结论:所以这个时候我们就会发现:原来 then 里面的函数和我们平时的回调函数一个意思,能够在 runAsync 这个异步任务执行完成之后被执行。

这里我们就可以清楚的知道 Promise 的作用了:异步执行的流程中,把原来的回调写法(执行代码和处理结果的代码)分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

下面我们再具体看看 Promise 相比于回调嵌套的写法的好处。

2、回调嵌套与Promise

从表面上看,Promise 只是能够简化层层回调的写法,而实质上,Promise 的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要简单、灵活的多。我们来看看这种简化解决了什么问题:

以往使用回调嵌套的方式来处理异步的代码是怎么实现的呢?

doA(function() {
  doB();
  doC(function() {
    doD();
  });
  doE();
});
doF();

//执行顺序:
//doA
//doF
//doB
//doC
//doE
//doD

这样组织的代码就会遇到一个问题:当项目的代码变得复杂,加上了各种逻辑判断,不断的在函数之间跳转,那排查问题的难度就会大大增加。就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,如果 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,难以定位问题。

回调嵌套:

request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});

使用 Promise 之后:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});

使用 Promise 的好处就非常明显了。

3、catch 方法

Promise 对象也拥有 catch 方法。它的用途是什么呢?其实它和 then 方法的第二个参数是一样的,用来指定reject 的回调,和写在 then 里第二个参数里面的效果是一样。用法如下:

runAsync()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(error){
    console.log('rejected');
    console.log(error);
});

catch 还有另外一个作用:在执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会程序报错卡死,而是会进到这个 catch 方法中,看个例子:

runAsync()
  .then(function(data) {
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
  })
  .catch(function(error) {
    console.log('rejected');
    console.log(error);
  });

// 执行完成
// resolved
// 数据
// rejected
// ReferenceError: somedata is not defined

resolve 的回调中,somedata 这个变量是没有被定义的。如果我们不用 catch,代码运行到这里就直接报错了,不往下运行了。但是在这里,会得到这样的结果。也就是说,程序执行到 catch 方法里面去了,而且把错误原因传到了 error 参数中。即便是有错误的代码也不会报错了,这与 try/catch 语句有相同的功能。所以,如果想捕获错误,就可以使用 catch 方法。

4、Promise.all()

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

看个例子:

function runAsync1() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('执行完成1');
      resolve('数据1');
    }, 2000);
  });
  return p;
}

function runAsync2() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('执行完成2');
      resolve('数据2');
    }, 2000);
  });
  return p;
}

function runAsync3() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('执行完成3');
      resolve('数据3');
    }, 2000);
  });
  return p;
}

Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function(results) {
  console.log(results);
});

// 执行完成1
// 执行完成2
// 执行完成3
// [ '数据1', '数据2', '数据3' ]

Promise.all 来执行,接收一个数组参数,里面的值最终都返回 Promise 对象。这样,三个异步操作的就是并行执行的,等到它们都执行完后才会进到 then 里面。那么,三个异步操作返回的数据哪里去了呢?都在 then 里面呢,Promise.all 会把所有异步操作的结果放进一个数组中传给 then ,就是上面的 results

应用场景:

Promise.all 方法有一个非常常用的应用场景:打开网页时,预先加载需要用到的各种资源如图片及各种静态文件,所有的都加载完后,再进行页面的初始化。

5、Promise.race()

race 是竞赛、赛跑的意思。它的用法也就是它的字面意思:谁跑的快,就以谁为准,执行回调。其实再看看Promise.all 方法,和 race 方法恰恰相反。还是用 Promise.all 的例子,但是把 runAsync1 的方法 timeout 时间调成 1000ms

Promise.race([runAsync1(), runAsync2(), runAsync3()]).then(function(results) {
  console.log(results);
});

// 执行完成1
// 数据1
// 执行完成2
// 执行完成3

这三个异步操作同样是并行执行的。结果很可以猜到,1秒后 runAsync1 已经执行完了,此时 then 里面的方法就会立即执行了。但是,在 then 里面的回调函数开始执行时,runAsync2()runAsync3() 并没有停止,仍然继续执行。所以再过1秒后,输出了他们结束的标志。这个点需要注意。

上面的这些方法就是 Promise 比较常用的几个方法了。

三、红绿灯问题

题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)

三个亮灯函数已经存在:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

利用 then 和递归实现:

function red() {
  console.log('red');
}
function green() {
  console.log('green');
}
function yellow() {
  console.log('yellow');
}

const light = function(timmer, color) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      color();
      resolve();
    }, timmer);
  });
};

const step = function() {
  Promise.resolve()
    .then(function() {
      return light(3000, red);
    })
    .then(function() {
      return light(2000, green);
    })
    .then(function() {
      return light(1000, yellow);
    })
    .then(function() {
      step();
    });
};

step();
发布了28 篇原创文章 · 获赞 11 · 访问量 2538

猜你喜欢

转载自blog.csdn.net/Deepspacece/article/details/104332287