felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

Promise 基础知识 #129

Open felix-cao opened 5 years ago

felix-cao commented 5 years ago

一、Promise 概述

1.1、Promise 的诞生

在没有 Promise 之前,我们的异步程序流程编码中,可能会遇到类似这样的情况:

这种回调函数嵌套 (Callback Nested)的现象经常被叫做 回调地狱 (Callback Hell),也被叫做 回调金字塔 (Pyramid of Doom),专指由于代码不断 嵌套/缩进 所形成的金字塔形状,嵌套/缩进越多金字塔形状越明显。

这种体验很差,在这种情况下诞生了 Promise,它最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,提供了原生 Promise 对象。

👍 目前,市面上各种浏览器对 ES6 的支持已经达到 90% 以上。

1.2、什么是 Promise

PromiseES6 提供的一种新的异步编程解决方案,完全改变了 JavaScript 异步编程的写法,让异步编程变得简洁优雅自然可读,比传统的解决方案——回调函数和事件更合理、更强大。最重要的是 可读性更友好

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

Promise 字面上理解即承诺的意思,new 一个 Promise 就是新建一个承诺。在新建一个承诺的时候你需要指定承诺是否实现的标准,即 Promise 构造器中设置一个函数作为参数,这个函数内部指定了承诺是否实现的标准。

1.3、Promise 的使用方法

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

var promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(
  function(success) {},
  function(error) {}
)

我们来看下步骤: 🔢

1、new 操作符调用 Promise 构造函数,并接受一个函数作为 Promise 构造函数的参数,该函数有两个参数,resolvereject, 而 resolvereject 是两个函数,由原生的 PromiseAPI 提供,不用自己部署。 2、resolve 用来将 Promise 对象的状态置为成功,并将异步操作结果 value 作为参数传递给成功回调函数 3、reject 用来将 Promise 对象的状态置为失败,并将异步操作结果 error 作为参数传递给失败回调函数 4、Promise 实例生成以后,可以调用then方法,并设置两个函数,即分别指定resolved状态和rejected状态的回调函数。通俗的讲,then 方法绑定两个回调函数,第一个用来处理 Promise 成功状态,第二个用来处理 Promise 失败状态,其中,第二个参数是可选的。

注意: 调用 resolvereject 并不会终结 Promise 的参数函数的执行。 👍

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代码中,调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolvedPromise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

一般来说,调用 resolvereject 以后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该直接写在 resolvereject 的后面。所以,最好在它们前面加上 return 语句,这样就不会有意外。 👍

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

二、Promise 状态及状态变化

下图是 Promise 对象的状态变化图

从上图我们可以看出,Promise 对象只有 三种状态: 🔢

Promise 对象的 状态改变 只有两种: 🔢

Promise 对象有两个特点: 1、对象状态只由异步操作结果决定。resolve 方法会使 Promise 对象由 pendding 状态变为 fulfilled状态;reject 方法或者异常会使得 Promise 对象由 pendding 状态变为 rejected 状态。Promise 状态变化只有上图这两条路径。 2、对象状态一旦改变,任何时候都能得到这个结果。即状态一旦进入 fulfilled 或者 rejectedpromise 便不再出现状态变化,同时我们再添加回调会立即得到结果。这点跟事件不一样,事件是发生后再绑定监听,就监听不到了。

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

但是 Promise 也有一些缺点: 🔢

三、使用 Promise 的流程

使用 Promise 的流程。

var p = new Promise(fn);
p.then(function(data) {
  console.log('resolve: ', data);
}, function(data) {
  console.log('reject: ', data);
})

function fn(resolve, reject) {
  console.log('begin to execute!');
  var number = Math.random();
  if(number<=0.5) {
    resolve('less than 0.5');
  } else {
    reject('greater than 0.5');
  }
}

这个例子当中,在 fn 当中产生一个0~1的随机数,如果小于等于0.5, 则调用resolve函数,大于0.5,则调用reject函数。函数定义好之后,用 Promise 包裹这个函数,返回一个 Promise 对象,然后调用对象的then方法,分别定义resolvereject函数。这里 resolvereject 比较简单,就是把传来的参数加一个前缀然后打印输出。

下面是一个 异步加载图片 的例子

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

上面代码中,使用 Promise 包装了一个图片加载的异步操作。如果加载成功,就调用 resolve 方法,否则就调用 reject 方法。

下面是一个用 Promise 对象实现的 Ajax 操作的例子:

const getRequest = function(url) {
  return new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };

    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
  });
};
const url = 'https://jsonplaceholder.typicode.com/todos/1';
getRequest (url).then(function(res) {
  console.log('Contents: ', res);
}, function(error) {
  console.error('出错了', error);
});

上面代码中,getRequest 是对 XMLHttpRequest 对象的封装,用于发出一个 HTTP 请求,并且返回一个 Promise 对象。需要注意的是,在 getRequest 内部,resolve 函数和 reject 函数调用时,都带有参数。

四、Promise.prototype.then() 方法

每一个 Promise 实例对象都有一个 then 方法,实际上 then 方法是定义在原型对象 Promise.prototype 上的函数,它的作用是为 Promise 实例对象设置状态改变时的回调函数。前面说过,then 方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调函数。

ps 关于原型和原型对象请阅读《JavaScript 原型及原型对象》

then 方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

const url = 'https://jsonplaceholder.typicode.com/todos/1';
getRequest (url).then(function(res) {
  return res.title;
}).then(function(res) {
  // ...
});

上面的代码使用 then 方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

五、Promise.prototype.catch() 方法

Promise.prototype.catch 方法是 .then(null, rejection).then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。

const url = 'https://jsonplaceholder.typicode.com/todos/1';
getRequest (url).then(function(res) {
  // ...
}).catch(function(error) {
  // 处理 getRequest 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getRequest 方法返回一个 Promise 对象,如果该对象状态变为 resolved,则会调用 then 方法指定的回调函数;如果异步操作抛出错误,状态就会变为 rejected,就会调用 catch 方法指定的回调函数,处理这个错误。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});

promise.catch(function(error) {
  console.log(error);
});
// Error: test

上面代码中,promise 抛出一个错误,就被 catch 方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。 👍

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

比较上面两种写法,可以发现 reject 方法的作用,等同于抛出错误。

如果 Promise 状态已经变成resolved,再抛出错误是无效的。 👍

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。 👍

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面 then 方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用 catch 方法,而不使用 then 方法的第二个参数。

Reference