Es6 Learning Notes 7th

Posted by Youzi Blog on March 26, 2019

ES6 新特性学习第七篇

前言

好久没写了,前几天把以前的 blog 重新看了看,继续学习 ES6。

第一篇前言

本文建立在学习阮一峰老师的 ES6 教程之上,总结了一些我自己认为重要的点,主要面向我本人,偏向于学习笔记的形式,主要参考 http://jsrun.net/tutorial/cZKKphttp://es6.ruanyifeng.com/

Promise

Promise 是一种异步的解决方案,传统的 JS 采取了回调函数的模式,这容易造成“回调地狱”的情况。Promise对象可以获取异步操作的消息,提供了统一的 API,各种异步操作都能用同样的方法进行处理。

Promise对象有 3 中状态:pending, fulfilled, rejected,只有异步操作的结果可以决定当前状态是哪种。状态改变只有两种情况:pending -> fulfilled, pending -> rejected只要这两种情况发生了,状态就固定了,此时称为 resolved;状态已经发生改变了,再对Promise对象添加回调函数,也会直接得到结果,所以相比较于事件处理,无论在何时都能得到回调的结果。

Promise实例对象的状态只会发生一次改变,当状态确定后就不会再改变了,也就是说在调用了resolve | reject函数后,再去调用这两个函数,或者再抛出异常,不会再改变实例的状态

立即执行resolve函数的Promise对象,在本轮事件循环的结束时进行,而setTimeout函数(仅在时间间隔为 0 秒的情况)是在下一轮时间循环开始时执行的;

基本语法

  • Promise是一个构造函数,所以可以用它来生成实例。构造函数接受一个函数作为参数,MDN 将这个函数称为executor
  • executor函数将两个函数resolve, reject作为参数(executor(resolve, reject)),两个函数被调用时会将实例的状态改变为fulfilled || rejected
  • executor内部会执行一些异步操作,执行完后就会调用上面两个函数的其中一个,对状态进行修改。
  • 如果executor函数抛出错误,状态会变成rejected,返回值会被忽略。
  • 生成的Promise实例,用then方法指定resolved || rejected状态的处理函数,其中reject函数是可选的。

来看个简单但是完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Promise((resolve, reject) => {
    // do something...
    if (/* success condition */) {
        // 一般异步操作成功的情况会调用resolve,失败调用reject
        resolve(params)
    } else {
        /* failed condition */
        reject(error)
    }
}).then((params) => {
    // resolve function
}, (error) => {
    // reject function
})

再来看一个更具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Promise((resolve, reject) => {
  let rand = Math.random();
  if (rand > 0.5) {
    setTimeout(() => resolve(`${rand} is greater than 0.5`), rand * 1000 * 3);
  } else {
    setTimeout(() => reject(`${rand} is less than 0.5`), rand * 1000 * 5);
  }
})
  .then(result => {
    console.log(result, 'resolved');
  })
  .catch(err => {
    console.log(err, 'rejected');
  });

由于每次调用then方法都会返回一个新的Promise的实例,所以可以使用链式写法在调用了then方法后再调用then方法;另外catch方法是在抛出异常执行reject时被调用的,其实和then方法传入错误处理函数是一样的。

Promise 建立后会立即执行then指定的回调函数,将在当前脚本所有同步任务执行完后才会执行。

1
2
3
4
5
6
7
8
9
10
new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
}).then(function() {
  console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved

resolve函数的参数可以是另一个Promise的实例

1
2
3
4
5
6
7
const p1 = new Promise(function(resolve, reject) {
  // ...
});
const p2 = new Promise(function(resolve, reject) {
  // ...
  resolve(p1);
});

这种情况p1的状态会传递给p2,导致了p2状态由p1决定,如果p1pending,那么p2会等待p1的状态改变;如果p1状态是另外两种状态,那么p2的回调函数会立刻执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p1 = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000);
});

const p2 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    console.log('???');
    resolve(p1);
  }, 1000);
});

p2.then(result => console.log(result)).catch(error => console.log(error));
// ???
// Error: fail

上面代码中,p1是一个Promise,3 秒之后变为rejectedp2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

另外,调用resolve || reject不会终止executor函数的执行,但是一般情况下调用了这俩函数之后就应该结束执行了,所以一般在调用这两个函数之后就return

Promise.prototype.then

前文提到了then方法可以用链式写法来调用,因为then方法会返回一个新的Promise对象;依照上面一段的代码,后一个then方法(或catch)会等待前一个的状态发生改变才会调用。

Promise.prototype.catch

catch方法可以说是then(null, rejection)的别名,用于指定发生错误时的回调函数。前面的例子也提到过了。但是要注意这个方法会捕获then方法抛出的异常,也更像是try/catch块的写法。因此建议都用下面代码的第二种写法,避免用then(null, rejection)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
promise.then(
  function(data) {
    // success
  },
  function(err) {
    // error
  }
);

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

Promise.prototype.finally

这个方法用于指定无论promise实例的状态如何都会执行的方法。有点像默认方法的意思,且finally方法总是返回原来的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// resolve 的值是 undefined
Promise.resolve(2).then(
  () => {},
  () => {}
);

// resolve 的值是 2
Promise.resolve(2).finally(() => {});

// reject 的值是 undefined
Promise.reject(3).then(
  () => {},
  () => {}
);

// reject 的值是 3
Promise.reject(3).finally(() => {});

上述代码描述了执行finally方法和then方法的区别,then方法会返回其方法本身返回的值,而finally方法会返回原来的值。

Promise.all Promise.race

两个方法的参数都是一组Promise实例,都是将这些实例包装成一个新的Promise实例。

方法的区别在于,all()方法会等所有参数的状态都变成fulfilled,新实例的状态才会变为fulfilled,或者有一个参数的状态变成rejected,实例的状态就变为rejectedrace()方法的某个参数的状态改变,实例的状态都会改变。

all方法在fulfilled状态下会返回一个数组,数组元素是各个参数的返回值;在rejected状态下会返回第一个被rejected参数的返回值。race方法的返回值是最先改变状态的参数的返回值。

一个应用场景:

1
2
3
4
5
6
7
8
const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function(resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000);
  })
]);

p.then(console.log).catch(console.error);

上述代码如果在 5 秒内fetch方法没得到返回,就会返回rejected,实例p的状态就会变成rejected

如果实例中有自己的catch方法,则不会将异常抛出到all | race方法中,且此时实例的状态都会变成fulfilled

来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p1 = new Promise((res, rej) => {
  rej('rejected');
}).catch(e => e);
const p2 = new Promise((res, rej) => {
  res('resolved');
});
Promise.all([p1, p2])
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.log(err);
  });
// ["rejected", "resolved"]

可以看出如果在子Promise实例中已经catch了异常,在all方法里就不会再次捕获。

Promise.allSettled

新增的方法,该方法接受一组Promise实例参数,只有参数都返回结果后(不论是fulfilled还是rejected),才会返回一个新的Promise实例,并且返回实例的状态总是fulfilled;由于返回值是实例对象,所以还需要访问then方法去取得返回数组,来看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const p1 = new Promise((res, rej) => {
  rej('rejected');
}).catch(e => e);
const p2 = new Promise((res, rej) => {
  res('resolved');
});
Promise.allSettled([p1, p2]).then(result => {
  console.log(result);
});

let num = Math.random();
const p3 = new Promise((res, rej) => {
  if (num >= 0.5) {
    res(num);
  } else {
    rej(num);
  }
});
num = Math.random();
const p4 = new Promise((res, rej) => {
  if (num >= 0.5) {
    res(num);
  } else {
    rej(num);
  }
});
let settled = await Promise.allSettled([p3, p4]);
console.log(settled);
// 0: {status: "rejected", reason: 0.3948534817154049}
// 1: {status: "fulfilled", value: 0.903275305994583}

// 0: {status: "fulfilled", value: "rejected"}
// 1: {status: "fulfilled", value: "resolved"}

可以看到返回的Promise实例中其实包含了返回值数组,数组元素都是对象,对应了执行参数后的返回值,status属性就是当前实例的状态,值是字符串fulfilled | rejected的其中一个;当状态是fulfilled时,对象有value属性,可以看到值是res函数传入的参数;状态是rejected时,对象有reason属性,值是rej传入的参数。

另外第一段代码,p1调用了rej方法然后catch了,这里就会返回新的Promise实例且实例的状态为fulfilled,所以调用allSettled方法后返回的对象状态属性还是fulfilled

Promise.any

新增的方法,接受一组Promise实例作为参数,只要有一个变成fulfilled状态,返回的实例就是fulfilled状态;如果所有的实例都变成rejected状态,那么返回的实例就是fulfilled状态;这个方法还处于提案阶段所以无法检验。

这几个方法类似于逻辑电路里的与非门,all方法是与的关系,并且有短路运算的特性;any方法是或的关系,不具备;race方法只是看哪个方法最快返回而不管返回状态;allSettled方法会等待所有参数状态确定

Promise.resolve

该方法用于将对象转化成Promise对象;根据参数的不同,总共有四种情况:

  • Promise 实例

由于参数本身就是实例对象了,所以会直接返回这个实例;

  • thenable对象

thenable对象是具有then方法的对象,resolve方法会把这个对象转换成Promise对象然后立即执行then方法,看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const thenable = {
  num: Math.random(),
  then(res, rej) {
    if (this.num >= 0.5) {
      res(this.num);
    } else {
      rej(this.num);
    }
  }
};
let p = Promise.resolve(thenable);
p.then(result => {
  // return result;
  console.log('resolved', result);
}).catch(err => {
  console.log('rejected', err);
  return err;
});
  • 参数没有then方法

这种情况包括了没有then方法的对象,原始值(数字、字符串等),此时调用resolve方法会返回一个新的Promise对象,状态为resolved,传入的参数作为value,看个例子:

1
2
3
4
5
6
7
8
9
10
11
let p = Promise.resolve('?');
p.then(val => {
  console.log(val);
});
// 等价于
let p = new Promise((res, rej) => {
  res('?');
});
p.then(val => {
  console.log(val);
});
  • 没有传入参数

不传入参数时,该方法直接返回一个resolved状态的Promise对象,比起new一个新的实例,再去调用res方法,这个方法比较简单;

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() {
  console.log('three');
}, 0);

Promise.resolve().then(function() {
  console.log('two');
});
console.log('one');
// one
// two
// three

可以看到立即执行then方法是微任务,会在宏任务执行完一轮后集中执行,且是在事件循环结束时执行,setTimeout方法会在下一轮时间循环开始时执行,所以会先输出two

Promise.reject

该方法一样会返回一个新的Promise实例,实例状态为rejected;函数参数是reason,即含义为失败的原因,不区分参数类型,不对参数进行处理,看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const o = {
  then(res, rej) {
    res();
  }
};
const s = 'rejected reason';
let p1 = Promise.reject(o);
let p2 = Promise.reject(s);
p1.catch(e => {
  console.log(Object.is(o, e));
  // true
  return Object.is(o, e);
});
p2.catch(e => {
  console.log(e);
  // rejected reason
  return e;
});

可以看出上面的代码中,即使参数是对象,也会原封不动的传递给catch函数

应用场景

加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const preloadImage = function(path) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
preloadImage('http://pic75.nipic.com/file/20150821/9448607_145742365000_2.jpg')
  .then(() => {
    console.log('?');
  })
  .catch(() => {
    console.log('??');
  });