上一次梳理了原型链和继承的知识,这两者在 JS 的面向对象编程中可以说是非常常见的,尤其是开发 React 中的类式组件次次都要接触到 (虽我目前还是主要使用 Vue)。
然而其实在 JS 中,promise 出现的次数也相当频繁。只要涉及到异步事件的处理,几乎随处可见的就是 promise 的身影。
在这个笔记中,我决定记录下我在学习 promise 的课程中,涉及的的相关知识。
引子
在了解 Promise 之前,我们也要弄清楚一些概念。
函数对象?实例对象?
了解了 JS 的原型和继承之后,我们知道:在 Javascript 里面,严格来说其实并没有“类”的概念。类的实现基本是靠构造函数和原型对象来进行模拟得到的。
一个大神的笔记中梳理了以下关系图,通过这张图,我们能够更好地理解,JS 中的(构造)函数对象和实例对象的关系。
在这里,也不再过多赘述函数对象、实例对象以及继承链的更多细节了 (上一个笔记已写)。简单来说如下:
- 函数对象:将函数作为对象使用时,即为函数对象。构造函数也是一个函数,只不过它于普通函数有点不同。
- 实例对象:通过
new
一个构造函数产生的对象称为实例对象,简称对象。
两种回调函数
同步回调函数
同步回调函数是立即执行的,完全执行完了才结束,它不会被放入回调队列中。常见的同步回调有:
- 数组遍历相关的回调函数
- Promise 中的
executor
函数
举一个例子,比如有以下的代码:
1 | const arr = [1,3,5] |
问上述两个 console.log
谁先执行?
答案是:先 console.log 数组元素
,再输出 forEach() 之后
。
异步回调函数
异步回调对比同步回调不同的是,他是放入回调队列中将来执行的,等到同步代码执行完之后才进行执行。
还是一个例子:
1 | setTimeout(() => { // 通过 setTimeout 可以创建一个异步回调 |
在这里先输出 setTimeout() 之后
,然后才输出 timeout callback()
。
备注
在异步回调中,还有微任务 (microtask) 和宏任务 (task) 之分。这里微任务、宏任务等概念涉及 JS 的事件循环 (event loop) 机制,暂不赘述。
简单来说就是,因为 JS 的本质是单线程的,所以 JS 有个单线程的执行机制。其顺序为:
执行一个宏任务 (先执行同步代码) => 执行所有微任务 => UI Render => 执行下一个宏任务 => 执行所有微任务 => UI Render => …
其中,setTimeout
就是宏任务,promise.then
和 mutation
等是微任务。
JS 中的 error
处理
JS 错误的类型
Error
:所有错误的父类型ReferenceError
:引用的变量不存在1
2// a 未定义
console.log(a) // 报错TypeError
:数据类型不正确的错误1
2
3
4
5
6let b;
console.log(b.xxx) // 报错
// b 不是 Object, 无 xxx 属性
b = {};
b.xxx() // 报错
// b 有 xxx 属性,但不是一个函数RangeError
:数据值不在其所允许的范围内1
2
3
4function fn() {
fn();
}
fn(); // 报错,因为栈空间溢出SyntaxError
:语法错误1
let a = """"; // 报错
错误处理
捕获错误:
try ... catch
1
2
3
4
5
6
7
8
9
10
11try {
let d;
console.log(d.xxx);
} catch (error) {
console.log(error);
}
// error 是一个对象 (Object)
// 此时 error 是 TypeError 的实例
// 这里的 error.__proto__ = Error抛出错误:
throw error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 一个栗子
function something() {
if (Date.now() % 2 === 1) {
console.log('当前时间为奇数,可以执行任务');
} else {
throw new Error('当前时间为偶数,无法执行任务')
}
}
// 在外面捕获异常
try {
something();
} catch (error) {
alert(error.message);
}
错误对象
错误对象包含两个主要属性:
message
属性:错误相关信息stack
属性:函数调用栈记录信息
promise 的理解和使用
什么是 promise?
抽象表达
Promise 是 JS 中进行异步编程的新的解决方案。旧的是纯回调形式。
具体表达
- 从语法上来说:Promise 是一个构造函数
- 从功能上来说:promise 对象用来封装一个异步操作并可以获取其结果
promise 的状态
状态改变
pending
变为resolved
pending
变为rejected
基本流程
基本使用
1 | // 1. 创建一个新的 promise 对象 |
为什么“promise”?
指定回调函数的方式更加灵活。
旧方法:必须在启动异步任务前指定回调函数;
使用 promise:启动异步任务 => 返回
promise
对象 => 给promise
对象绑定回调函数,甚至可以在异步任务完成之后绑定。
支持链式调用,可以解决回调地狱的问题。回调地狱:回调函数嵌套调用,不便于阅读,不便于异常处理;
- 解决方案:
promise
链式调用; - 终极解决方案:
async / await
。
- 解决方案:
回调地狱 demo
1 | // 2a. 回调地狱的一个 demo |
肉眼可见,在有非常多的回调函数的时候,这种逻辑和不断缩进的代码会弄的人非常头大。
1 | // 2b. 使用 promise 的链式调用,可以解决回调地狱 |
在使用 promise 的链式调用之后,整个代码的逻辑变得清晰很多。
1 | // 2c. async/await:回调地狱的终极解决方案 |
将异步操作用 async/await
进行书写,就和写同步操作一样简单。
如何使用 promise
构造函数
Promise
的构造函数:Promise(executor) {}
executor
函数:同步执行(resolve, reject) => {}
resolve
函数:内部定义成功时我们调用的函数value => {}
reject
函数:内部定义失败时我们调用的函数reason => {}
说明:executor
会在 Promise
内部立即同步回调,异步操作是在执行器中执行的。
then
方法
Promise.prototype.then
方法:(onResolved, onRejected) => {}
onResolved
函数:成功的回调函数(value) => {}
onRejected
函数:失败的回调函数(reason) => {}
说明:设置用于得到成功 value
的成功回调和用于得到失败 reason
的失败回调,返回一个新的 promise
对象。
catch
方法
Promise.prototype.catch
方法:(onRejected) => {}
onRejected
函数:失败的回调函数(reason) => {}
说明:then()
的语法糖,相当于 then(undefined, onRejected)
。
resolve
方法
Promise.resolve(value)
方法:(value) => {}
value
:成功的数据或promise
对象
1 | // 产生一个成功值为 1 的 promise 对象 |
说明:返回一个成功/失败的 promise
对象。
reject
方法
Promise.reject
方法:(reason) => {}
reason
:失败的原因
说明:返回一个失败的 promise
对象。
all
方法
Promise.all
方法:(promises) => {}
promises
:包含 n 个promise
的数组
1 | const pAll = Promise.all([p1, p2, p3]); |
说明:返回一个新的 promise
,只有所有的 promise
都成功才算成功,只要有一个失败了就直接失败。
race
方法
Promise.race
方法:(promises) => {}
promises
:包含 n 个promise
的数组
说明:返回一个新的 promise
,第一个完成的 promise
的结果状态就是最终的结果状态。
注意点
如何改变 promise
的状态?
resolve(value)
:如果当前状态是pending
,就会变为resolved
reject(reason)
:如果当前状态是pending
,就会变为rejected
抛出异常:如果当前状态是
pending
,就会变为rejected
注意:这里可以
throw
任何东西,并不一定要是一个error
。比如说除了throw new Error('出错了')
以外,也可以throw 3
一个 promise
指定多个成功/失败回调函数,都会调用吗?
Yes。当 promise
改变为对应状态时都会调用。
改变 promise
状态和指定回调函数,谁先谁后?
都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调;
如何先改状态再指定回调?
- 在执行器中直接调用
resolve()
/reject()
- 延迟更长时间才调用
then()
- 在执行器中直接调用
什么时候才能得到数据?
如果先指定回调函数,那就当状态发生改变时,回调函数就会调用,得到数据;
1
2
3
4
5
6
7
8new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1); // 后改变的状态 (同时指定数据)
}, 1000);
}).then( // 先指定回调函数,保存当前指定的回调
value => {},
reason => {console.log('reason', reason)}
)如果先改变状态,那当指定回调函数时,回调函数就会调用,得到数据。
1
2
3
4
5
6new Promise((resolve, reject) => {
resolve(1); // 先改变的状态 (同时指定数据)
}).then( // 后指定回调函数,异步执行回调函数
value => {},
reason => {console.log('reason', reason)}
)
promise.then()
返回新的结果状态
例如有以下的代码,试问其执行结果是什么
1 | new Promise((resolve, reject) => { |
输出为
1 | onResolved1() 1 |
那么,promise.then()
返回的新 promise
的结果状态,由什么决定?
- 简单表达:由
then()
指定的回调函数执行的结果决定; - 详细表达:
- 如果抛出异常,新
promise
变为rejected
,reason
为抛出的异常 - 如果返回的是非
promise
的任意值,新promise
变为resolved
,value
为返回的值 - 如果返回的是另一个新
promise
,此promise
的结果就会成为新promise
的结果
- 如果抛出异常,新
promise
如何串联多个操作任务?
promise
的then()
返回一个新的 promise,可以看成then()
的链式调用;注意:同步操作可以
return
值,但异步操作必须包裹一个新的promise
对象通过
then
的链式调用串连多个同步/异步任务。
promise
异常穿透?
- 当使用
promise
的then
链式调用时,可以在最后指定失败的回调; - 前面任何操作发生异常,都会传到最后失败的回调中处理。
如何中断 promise
链?
- 当使用
promise
的then
链式调用时,在中间中断,不再调用后面的回调函数; - 方法:在回调函数中返回一个
pending
状态的promise
对象。
1 | new Promise((resolve, reject) => { |