Simonzhangs' blog Simonzhangs' blog
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Simonzhangs

前端学习探索者
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JS基础

    • typeof与instanceof
    • JS变量声明的六种方式
    • this指向
    • ==和===运算符区别
    • Promise基础
      • Promise 特性之 then、catch、finally:
      • 几道常见面试题
    • JS异步发展
  • 基础
  • 内置对象
  • 面向对象
  • 异步操作
  • DOM
  • 事件
  • 浏览器模型
  • JS常见高级函数
  • 《JavaScript教程》笔记
  • JS基础
simonzhangs
2022-04-22
目录

Promise基础

# Promise 特性之 then、catch、finally:

  1. Promise 的状态一经改变就不能再改变;
const promise = new Promise((resolve, reject) => {
  resolve("success1");
  reject("error");
  resolve("success2");
});
promise
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });
// "then: success1"
1
2
3
4
5
6
7
8
9
10
11
12
13

解析:构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用;即 Promise 的状态一经改变就不能再改变。

  1. catch不管被链接到哪里,都能捕获上层未捕获过的错误;.then和.catch都会返回一个新的 Promise;
const promise = new Promise((resolve, reject) => {
  reject("error");
  resolve("success2");
});
promise
  .then((res) => {
    console.log("then1: ", res);
  })
  .then((res) => {
    console.log("then2: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  })
  .then((res) => {
    console.log("then3: ", res);
  });
// "catch: " "error"
// "then3: " undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解析: catch 不管连接到哪里,都可以捕获上层未捕获过的错误;then3 被执行是因为 catch()也会返回一个 Promise,且由于这个 Promise 没有返回值,所以打印出来的是 undefined。(因为 catch 到了错误,所以返回的 Promise 是成功态,但是没有返回值,故为 undefined)

  1. 在 Promise 中,返回任意一个非promise的值都会包裹成 promise 对象;例如return 2会被包装成:return Promise.resolve(2);
Promise.resolve(1)
  .then((res) => {
    console.log(res);
    return 2;
  })
  .catch((err) => {
    return 3;
  })
  .then((res) => {
    console.log(res);
  });
// 1
// 2
1
2
3
4
5
6
7
8
9
10
11
12
13

解析:Promise 可以链式调用,不过 promise 每次调用.then或者.catch都会返回一个新的 promise,从而实现链式调用,它并不像一般我们任务的链式调用一样return this。

上面的输出结果之所以依次打印出 1 和 2,那是因为 resolve(1)之后走的是第一个 then 方法,并没有走 catch 里,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值 return 2,且 return 2 会被包装成 resolve(2)。

同理,把上述题目中 Promise.resolve(1)改 Promise.reject(1):

Promise.reject(1)
  .then((res) => {
    console.log(res);
    return 2;
  })
  .catch((err) => {
    console.log(err);
    return 3;
  })
  .then((res) => {
    console.log(res);
  });
// 1
// 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解析:打印的当然是 1 和 3 啦,因为 reject(1)此时走的就是 catch,且第二个 then 中的 res 得到的就是 catch 中的返回值。

  1. Promise 的.then或者.catch可以被调用多次;但如果 Promise 内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("timer");
    resolve("success");
  }, 1000);
});
const start = Date.now();
promise.then((res) => {
  console.log(res, Date.now() - start);
});
promise.then((res) => {
  console.log(res, Date.now() - start);
});
// 'timer'
// 'success' 1001
// 'success' 1002
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

解析:Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。

  1. .then 或者.catch 中 return 一个 erro 对象并不会抛出错误,所以不会被后续的.catch 捕获;
Promise.resolve()
  .then(() => {
    return new Error("error!!!");
  })
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });
// "then: " "Error: error!!!"
1
2
3
4
5
6
7
8
9
10
11

解析: 它走的是.then,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也会被包裹成return Promise.resolve(new Error('error!!!'))。

提示

如果要抛出一个错误的,建议采用:return Promise.reject(new Error('error!!!'));或throw new Error('error!!!')。

  1. .then 或.catch 返回的值不能是 promise 本身,否则会造成死循环。
const promise = Promise.resolve().then(() => {
  return promise;
});
promise.catch(console.err);
1
2
3
4

上述程序会报错:

Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
1
  1. .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传;
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
// 1
1
2

解析:第一个 then 和第二个 then 中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将 resolve(1) 的值直接传到最后一个 then 里。

  1. .then 函数中存在两个参数,第一个参数是用来处理 Promise 成功的函数,第二个则是处理失败的函数;
Promise.reject("error!!!")
  .then(
    (res) => {
      console.log("success", res);
    },
    (err) => {
      console.log("error", err);
    }
  )
  .catch((err) => {
    console.log("catch", err);
  });
// 'error' 'error!!!'
1
2
3
4
5
6
7
8
9
10
11
12
13

解析:它进入了.then()中的第二个参数里面,如果把第二个参数去掉,就会进入 catch()中,因为 catch 会捕获上层所有未捕获的错误。

但是如果在 then 的第一个参数中丢出错误呢?

Promise.resolve()
  .then(
    function success(res) {
      throw new Error("error!!!");
    },
    function fail1(err) {
      console.log("fail1", err);
    }
  )
  .catch(function fail2(err) {
    console.log("fail2", err);
  });
// fail2 Error: error!!!
// 			at success (C:\Users\zhang\Desktop\01.js:60:11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解析:由于 Promise 调用的是 resolve(),因此.then()执行的应该是 success()函数,可是 success()函数抛出的是一个错误,它会被后面的 catch()给捕获到,而不是被 fail1 函数捕获。

  1. .finally()方法特点:
  • .finally()方法不管 Promise 对象最后的状态如何都会执行;
  • .finally()方法的回调函数不接受任何的参数,所以说在.finally()中的函数中是没办法知道 Promise 最终的状态是 resolved 还是 rejected 的;
  • 它返回的默认是上一次的 Promise 对象,不过如果抛出的是一个异常则返回的是异常的 Promise 对象。
Promise.resolve("1")
  .then((res) => {
    console.log(res);
  })
  .finally(() => {
    console.log("finally");
  });
Promise.resolve("2")
  .finally(() => {
    console.log("finally2");
    return "我是finally2返回的值";
  })
  .then((res) => {
    console.log("finally2后面的then函数", res);
  });
// '1'
// 'finally2'
// 'finally'
// 'finally2后面的then函数' '2'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解析:这两个 Promise 的 finally 都会执行,及时 finally2 返回了值,后面 then 函数接收到的结果仍然是'2';finally2 在 finally 之前执行,是因为 finally 需要等到第一个 Promise.then()执行完毕后才会放入到微任务队列中。

如果在 finally 中抛出异常,

Promise.resolve("1")
  .finally(() => {
    console.log("finally1");
    throw new Error("我是finally中抛出的异常");
  })
  .then((res) => {
    console.log("finally后面的then函数", res);
  })
  .catch((err) => {
    console.log("捕获错误", err);
  });
// 'finally1'
// '捕获错误' Error: 我是finally中抛出的异常
1
2
3
4
5
6
7
8
9
10
11
12
13

如果改成 return New Error,打印出来的就是finally后面的then函数 1。

下面这个例子涉及到多次链式调用:

function promise1() {
  let p = new Promise((resolve) => {
    console.log("promise1");
    resolve("1");
  });
  return p;
}
function promise2() {
  return new Promise((resolve, reject) => {
    reject("error");
  });
}
promise1()
  .then((res) => console.log(res))
  .catch((err) => console.log(err))
  .finally(() => console.log("finally1"));

promise2()
  .then((res) => console.log(res))
  .catch((err) => console.log(err))
  .finally(() => console.log("finally2"));
// promise1
// 1
// error
// finally1
// finally2
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

解析:这里要注意的点:链式调用后面的内容需要等前一个调用执行完才会加入队列执行。

# 几道常见面试题

  1. 使用 Promise 实现每隔一秒输出 1,2,3

通过 reduce 使用 Promise.resolve()构造连续 Promise 回调实现系列异步请求按顺序执行:

const arr = [1, 2, 3, 4];
arr.reduce((p, x) => {
  return p.then(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(console.log(x));
      }, 1000);
    });
  });
}, Promise.resolve());
1
2
3
4
5
6
7
8
9
10

通过 async、await 和 for 循环来实现

const arr = [1, 2, 3, 4];
async function go(arr) {
  for (let i = 0; i < arr.length; i++) {
    await new Promise((resolve) => {
      setTimeout(() => {
        resolve(console.log(x));
      }, 1000);
    });
  }
}
go(arr);
1
2
3
4
5
6
7
8
9
10
11
  1. 使用 Promise 实现红绿灯交替重复亮
function red() {
  console.log("red 3s");
}
function yellow() {
  console.log("yellow 2s");
}
function green() {
  console.log("green 1s");
}

function light(timer, callback) {
  return new Promise((resolve) => {
    return setTimeout(() => {
      callback();
      resolve();
    }, timer);
  });
}

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

setp();
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
  1. 实现 mergePromise 函数
const time = (timer) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timer);
  });
};

const ajax1 = () =>
  time(2000).then(() => {
    console.log(1);
    return 1;
  });
const ajax2 = () =>
  time(1000).then(() => {
    console.log(2);
    return 2;
  });
const ajax3 = () =>
  time(1000).then(() => {
    console.log(3);
    return 3;
  });

function mergePromise(ajaxArr) {
  const data = [];
  let p = Promise.resolve();
  ajaxArr.forEach((ajax) => {
    p = p.then(ajax).then((res) => {
      data.push(res);
      return data;
    });
  });
  return p;
}

mergePromise([ajax1, ajax2, ajax3]).then((data) => {
  console.log("done");
  console.log(data); // data 为 [1,2,3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1,2,3]
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
34
35
36
37
38
39
40
41
42
43
44
45
46
  1. 封装一个异步加载图片的方法
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      console.log("img loading sucess");
      resolve(img);
    };
    img.onerror = () => {
      reject(new Error("Could not load image at " + url));
    };
    img.src = url;
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 限制异步操作的并发个数并尽可能快地完成

给定八个图片 url 地址的数组,要求同时加载图片不超过 3 个,并且尽可能快地完成图片的加载;如果采用 Promise.all 的话,有可能单个图片加载很慢,会阻塞整组的加载,从而会影响下一组的加载,可以通过 Promise.race()方法来一直不断更新加载,最后三个用 all 方法。(单个图片加载采用 4 中方法)

function limitLoad(urls, handler, limit) {
  const sequence = [].concat(urls);
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => index);
  });

  return sequence
    .reduce((p, url) => {
      return p
        .then(() => {
          return Promise.race(promises);
        })
        .then((fastestIndex) => {
          promises[fastestIndex] = handler(url).then(() => fastestIndex);
        })
        .catch((err) => {
          console.log(err);
        });
    }, Promise.resolve())
    .then(() => {
      return Promise.all(promises);
    });
}

limitLoad(urls, loadImg, 3)
  .then((res) => {
    console.log("图片全部加载完毕");
    console.log(res);
  })
  .catch((err) => {
    console.error(err);
  });
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
  1. for + setTimeout 输出问题
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(new Date(), i);
  }, 1000);
}
1
2
3
4
5

在了解了 JS 的事件循环之后,很容易得出答案:5 -> 5,5,5,5,5,先输出 5,然后隔 1s 输出 5 个 5

如何让输出结果变为5 -> 0,1,2,3,4呢?可以将 var 换成 let,利用 ES6 的块级作用域,不过第一个 5 就打印不出来了。

① 可以通过立即执行函数来解决闭包造成的问题:

for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(new Date(), i);
    }, 1000);
  })(i);
}

console.log(new Date(), i);
1
2
3
4
5
6
7
8
9

② 也可以利用 JS 基本类型的参数传递是按值传递的特征:

var output = (i) => {
  setTimeout(() => {
    console.log(new Date(), i);
  }, 1000);
};
for (var i = 0; i < 5; i++) {
  output(i);
}

console.log(new Date(), i);
1
2
3
4
5
6
7
8
9
10

③ 利用 Promise 来处理异步事件

const tasks = [];
const output = (i) =>
  new Promise((resolve) => {
    setTimeout(() => {
      console.log(new Date(), i);
      resolve();
    }, 1000 * i);
  });

for (var i = 0; i < 5; i++) {
  tasks[i] = output(i);
}

Promise.all(tasks).then(() => {
  setTimeout(() => {
    console.log(new Date(), i);
  }, 1000);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

④ 使用 ES7 async

function sleep = (ms) => new Promise((resolve) => {
  setTimeout(()=> {
    resolve();
  }, ms)
})

( async () => {
  for(var i=0;i<5;i++) {
    await sleep(1000);
    console.log(new Date,i);
  }
  await sleep(1000);
  console.log(new Date,i);
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

参考:

要就来 45 道 Promise 面试题一次爽到底(1.1w 字用心整理) (opens new window) 80% 应聘者都不及格的 JS 面试题 (opens new window)

编辑 (opens new window)
上次更新: 2022/05/04, 21:37:45
==和===运算符区别
JS异步发展

← ==和===运算符区别 JS异步发展→

最近更新
01
一些有意思的类比
06-16
02
the-super-tiny-compiler解析
06-06
03
计算机编译原理总概
06-06
更多文章>
Theme by Vdoing | Copyright © 2021-2022
蜀ICP备2021023197号-2
Simonzhans | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜