Promise从入门到放弃
Event Loop
在讲解 Promise 之前,我们先来了解下 Event Loop(事件循环 / 事件轮询)。
我们都知道 JavaScript 是单线程的,单线程也就意味着所有的任务都需要排队,只有当前一个任务结束后,才会去执行后一个任务。如果前一个任务所耗时长很久,就容易导致后面的任务堵塞。
我们可以把这些任务分为两类,即 同步任务和异步任务。
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕后,才能执行后面一个任务;
- 异步任务:不进入主线程、而进入 "任务队列" 的任务,只有 "任务队列" 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JavaScript 执行步骤:
- 从前到后,依次执行;
- 如果某一行报错,后面的代码将不会执行;
- 先执行同步任务,再执行异步任务。
我们先来看如下代码,执行顺序是怎样的呢:
console.log(1);
setTimeout(function fn() {
console.log(2);
}, 5000)
console.log(3);
是不是很简单,执行结果为:1 3 2。
下面我们就上面这个例子来讲解一下 event loop 的执行过程。
我们可以在 latentflip.com/loupe 这个网站查看具体的执行过程。
我们先来看如下图每个区域的含义:
再来看看具体的执行过程:
- 将
console.log(1)
放进 Call Stack 后执行打印出 1,然后退出; - 将 setTimeout 放进 Call Stack 后,然后把 fn() 放进 Web Apis 里后 setTimeout 退出;
- 将
console.log(3)
放进 Call Stack 后执行打印出 3,然后退出; - 此时所有同步代码已经执行完毕,Call Stack 里不会再有代码进入,然后进行 event loop 机制,它会一遍又一遍的进行循环,从 Callback Quese 里去找有没有函数,如果有就放入 Call Stack 里去执行;
- 当 fn() 在 Web Apis 里呆了 5s 后会自动进入 Callback Quese 里;
- 此时 event loop 会发现 Callback Quese 里有个 fn(),会立刻把 fn() 推到 Call Stack 里,并立即触发 fn() 的执行;
- 然后把
console.log(2)
放进 Call Stack 后执行打印出 2,然后退出; - 最后退出 fn(),清空 Call Stack。
下面我们简洁的总结下上面的执行过程:
- 把同步代码一行一行的放进 Call Stack 里去执行;
- 如果遇到异步代码,会放到 Web Apis 里记录一下,并且等待时机成熟后放进 Callback Quese 里;
- 如果同步代码执行完后(即 Call Stack 为空),event loop 就可以开始上班了;
- 循环查找 Callback Quese,如有则移动至 Call Stack 里去执行,然后再继续循环查找(找呀找呀找😱)。
下面我们再来看一个例子,结合 DOM 事件,代码如下:
console.log(1)
setTimeout(function fn() {
console.log(2);
}, 5000)
$.on('button', 'click', function onClick() {
console.log('click');
});
console.log(3);
执行过程如下图:
当我们点击一下后,会把 onClick() 放入 Callback Quese 里。如果点 n 下,就会放 n 个 onClick() 到 Callback Quese 里。
Event Loop 其实就是异步回调的实现原理。
现在是不是对 Event Loop 的执行过程了解了呢?😂
宏任务与微任务
宏任务(macrotask):setTimeout、setInterval、Ajax、DOM事件;
微任务(microtask):Promise、async / await。
注意:微任务执行时机要比宏任务早!!
下面我们来看一个例子:
console.log(1);
setTimeout(() => {
console.log(2);
})
Promise.resolve().then(() => {
console.log(3);
})
console.log(4);
如果我们了解宏任务与微任务,那么结果显而易见:1 4 3 2。
再来看一个例子:
setTimeout(() => {
console.log(4)
})
new Promise(resolve => {
resolve()
console.log(1)
}).then(() => {
console.log(3)
})
console.log(2)
首先,new Promise 在实例化的过程中所执行的代码都是同步进行的,而 .then 中注册的回调才是异步执行的。所以先打印 1,其次是 2。又因为同步代码执行完成后才会去执行异步任务,并且微任务会在宏任务之前执行。因此,最后打印结果为:1 2 3 4。
Promise的理解与使用
Promise的理解
Promise是什么
抽象的表达就是:Promise 是异步编程的一种解决方案。
而具体点的表达就是:
- 在语法上:Promise 是一个构造函数;
- 在功能上:Promise 对象是用来封装一个异步操作并可以从中获取结果;
Promise的状态
Promise 只有三种状态,分别为初始状态(pending)、成功状态(fulfilled / resolved)、失败状态(rejected)。
- pending(初始状态): 初始状态,既不是成功,也不是失败状态;
- fulfilled / resolved(已成功): 意味着操作成功完成;
- rejected(已失败): 意味着操作失败;
而在这三种状态之间,Promise 对象的状态改变只有两种情况,分别是:
- 从初始状态(pending)变为成功状态(fulfilled / resolved);
- 从初始状态(pending)变为失败状态(rejected);
只要上面这两种情况发生了,状态就凝固了,不会再变了,会一直保持这个结果。
一个 Promise 对象只能改变一次状态,无论最后是变成成功还是失败,都会有一个数据结果。
为什么要用Promise
使用 Promise 的原因有如下两点:
- 指定回调函数的方式更加灵活
- 支持链式调用,可以解决回调地狱的问题
那么回调地狱又是什么呢?
回调地狱即回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调的执行条件。
而回调地狱十分不便于开发者阅读,也不利于进行异常处理。因此,使用 Promise 的链式调用可以解决此类问题。
Promise的基本使用
Promise 的基本书写语法大致如下:
function fn() {
return new Promise((resolve, reject) => {
成功时调用 resolve(value)
失败时调用 reject(error)
})
}
fn().then(成功函数1, 失败函数1).then(成功函数2, 失败函数2)
let p = new Promise((resolve, reject) => {
setTimeout(() => {
let time = Date.now()
console.log(time)
if (time % 2 === 1) {
resolve(time)
} else {
reject(time)
}
}, 1000)
})
p.then((value) => {
console.log('成功的回调' + value)
}, (error) => {
console.log('失败的回调' + error)
})
Promise的API
Promise.prototype.then
Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。该方法返回一个新的 promise 对象。
它的作用是为 Promise 实例添加状态改变时的回调函数。then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
let p1 = new Promise((resolve, reject) => {
...
})
p1.then(value => {...}, error => {...})
Promise.prototype.catch
Promise.prototype.catch() 方法是 .then(null, rejected) 或 .then(undefined, rejected) 的别名,用于指定发生错误时的回调函数。
let p1 = new Promise((resolve, reject) => {
reject('error')
})
p1.then(value => {
console.log(value)
}).catch(error => {
console.log(error)
})
Promise.resolve
Promise.resolve() 返回一个成功 Promise 对象。
// value: 成功的数据或 Promise 对象
Promise.resolve(value)
let p1 = Promise.resolve('p1')
// 等价于
let p1 = new Promise(resolve => {
resolve('p1')
})
Promise.reject
Promise.reject() 返回一个失败的 Promise 对象。
// error: 失败的原因
Promise.reject(error)
let p2 = Promise.reject('error')
// 等价于
let p2 = new Promise((resolve, reject) => {
reject('error')
})
Promise.all
Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。传入的参数为数组形式。
该 Promise 对象在数组参数对象里所有的 Promise 对象都成功的时候才会触发成功,一旦有任何一个数组参数对象里面的 Promise 对象失败则立即触发该 Promise 对象的失败。
// promise1 、promise2 、 promise3 都成功才会调用成功函数,否则调用失败函数
Promise.all([promise1, promise2, promise3, ...]).then(成功函数, 失败函数)
let p1 = new Promise((resolve, reject) => {
resolve('p1')
})
let p2 = new Promise((resolve, reject) => {
resolve('p2')
})
let p3 = new Promise((resolve, reject) => {
reject('p3')
})
let pAll1 = Promise.all([p1, p2, p3]).then(value => {
console.log(value)
}, error => {
console.log(error) // p3
})
let pAll2 = Promise.all([p1, p2]).then(value => {
console.log(value) // (2) ["p1", "p2"]
}, error => {
console.log(error)
})
Promise.all() 返回一个新的 Promise, 只有所有的 Promise 都成功才成功, 只要有一个失败了就直接失败。
Promise.race
Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。传入的参数为数组形式。
race 翻译过来大致就是“比赛”的意思。因此,第一个完成的 Promise 的结果状态就是最终的结果状态。
// 如果 promise1 为成功状态并先完成,那么最终将执行成功函数;如果 promise1 为失败状态并先完成,那么最终将执行失败函数。
Promise.race([promise1, promise2, promise3, ...]).then(成功函数, 失败函数)
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2')
}, 2000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('p3')
}, 3000)
})
Promise.race([p1, p2, p3]).then(value => {
console.log(value) // p1
}, error => {
console.log(error)
})
Promise.allSettled
Promise.allSettled() 方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。
该方法返回的新的 Promise 实例,一旦结束,状态总是 fulfilled,不会变成 rejected。状态变成 fulfilled 后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入 Promise.allSettled() 的 Promise 实例。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello")
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("error")
}, 1000)
})
let res = Promise.allSettled([p1, p2])
console.log(res) // Promise {<pending>}
then与catch对状态的影响
- then 正常返回 resolved,里面有报错则返回 rejected;
- catch 正常返回 resolved,里面有报错则返回 rejected。
我们首先来看看下面这段代码来理解第一条的意思。
let p1 = Promise.resolve().then(() => {
return 10
})
console.log('p1', p1) // fulfilled状态
p1.then(() => {
console.log("可以触发then") // 执行
})
let p2 = Promise.resolve().then(() => {
throw new Error('err')
})
console.log('p2', p2) // rejected状态
p2.then(() => {
console.log("不可以触发then") // 不执行
}).catch(() => {
console.log("可以触发catch") // 执行
})
看了上面代码是不是 so easy,下面我们来看看第二条的意思。
let p3 = Promise.reject('err').catch(err => {
console.log(err)
})
console.log('p3', p3) // fulfilled状态
p3.then(() => {
console.log('可以触发then') // 执行
})
let p4 = Promise.reject('err').catch(err => {
throw new Error('err')
})
console.log('p4', p4) // rejected状态
p4.then(() => {
console.log('不可以触发then') // 不执行
}).catch(() => {
console.log('可以触发catch') // 执行
})
如果 catch 里的代码没有报错,那么将会返回 fulfilled 状态,如果报错将是 rejected 状态。是不是感觉这条很绕呢😂
手写Promise
Promise相关问题与题目
如何改变Promise的状态
Promise 状态的改变除了上面所说的从 pendding 变为 resolved 和 pendding 变为 rejected 之外,还有一种可以改变状态,那就是抛出异常,会从 pendding 就会变为 rejected。
let p = new Promise((resolve, reject) => {
throw new Error('error')
})
p.then(
value => {},
error => {
console.log(error) // Error: error
}
)
改变Promise状态和指定回调函数的谁先谁后
都有可能,一般情况下先指定回调函数然后再改变的状态,但是也可也先改变状态然后再指定回调函数
如下两种方法可以先改变状态然后再指定回调函数:
- 在执行器中直接调用 resolve() / reject()
- 添加 setTimeout 延迟更长的时间调用 .then()
// 先指定的回调函数,并保存当前指定的回调函数。然后改变的状态,再执行异步回调函数
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1')
}, 1000)
})
p1.then(value => {
console.log(value)
}, error => {
console.log(error)
})
// 先改变的状态,然后指定回调函数,并异步执行回调函数
let p2 = new Promise((resolve, reject) => {
resolve('p2')
})
p2.then(value => {
console.log(value)
}, error => {
console.log(error)
})
// 先改变的状态,然后指定回调函数,并异步执行回调函数
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p3')
}, 1000)
})
setTimeout(() => {
p3.then(value => {
console.log(value)
}, error => {
console.log(error)
})
}, 1200)
.then()返回的新Promise的结果状态由什么决定
- 如果返回的是非 Promise 的任意值, 新 Promise 变为 resolved, value 为返回的值;
- 如果返回的是另一个新 Promise, 那么这个 Promise 的结果就会成为新 Promise 的结果 ;
- 如果抛出异常, 新 Promise 变为 rejected, error 为抛出的异常;
new Promise((resolve, reject) => {
resolve('hh')
}).then(value => {
console.log('demo1,' + value) // demo1,hh
}, error => {
console.log('demo1,' + error)
}).then(value => {
console.log('demo2,' + value) // demo2,undefined
}, error => {
console.log('demo2,' + error)
})
new Promise((resolve, reject) => {
resolve('hh')
}).then(value => {
console.log('demo1,' + value) // demo1,hh
return 1
}, error => {
console.log('demo1,' + error)
}).then(value => {
console.log('demo2,' + value) // demo2,1
}, error => {
console.log('demo2,' + error)
})
new Promise((resolve, reject) => {
resolve('hh')
}).then(value => {
console.log('demo1,' + value) // demo1,hh
return Promise.resolve('3')
}, error => {
console.log('demo1,' + error)
}).then(value => {
console.log('demo2,' + value) // demo2,3
}, error => {
console.log('demo2,' + error)
})
new Promise((resolve, reject) => {
resolve('hh')
}).then(value => {
console.log('demo1,' + value) // demo1,hh
return Promise.reject('4')
}, error => {
console.log('demo1,' + error)
}).then(value => {
console.log('demo2,' + value)
}, error => {
console.log('demo2,' + error) // demo2,4
})
new Promise((resolve, reject) => {
resolve('hh')
}).then(value => {
console.log('demo1,' + value) // demo1,hh
throw 5
}, error => {
console.log('demo1,' + error)
}).then(value => {
console.log('demo2,' + value)
}, error => {
console.log('demo2,' + error) // demo2,5
})
下面代码执行结果是什么
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
参考答案
答:1 3。首先 resolve 返回 fulfilled 状态,因此执行第一个 then,打印 1;打印后还是 fulfilled 状态,所以不执行 catch 里面的代码,而是执行后面 then 的代码,打印出 3。
Promise.resolve().then(() => {
console.log(1)
throw new Error('err')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
参考答案
答:1 2 3。首先 resolve 返回 fulfilled 状态,因此执行第一个 then,打印 1;打印后执行下一行代码,将报错,所以第一个 then 返回的是 rejected 状态的 Promise,所以后面该执行 catch 里面的代码,打印出 2;catch 代码执行完成后变为 fulfilled 状态,因此再执行最后一个 then,打印出 3。
Promise.resolve().then(() => {
console.log(1)
throw new Error('err')
}).catch(() => {
console.log(2)
}).catch(() => {
console.log(3)
})
参考答案
答:1 2。首先 resolve 返回 fulfilled 状态,因此执行第一个 then,打印 1;打印后执行下一行代码,将报错,所以第一个 then 返回的是 rejected 状态的 Promise,所以后面该执行第一个 catch 里面的代码,打印出 2;catch 代码执行完成后变为 fulfilled 状态,因此不会执行最后一个 catch 里面的代码。
console.log("1");
setTimeout(() => {
console.log("2")
}, 1000);
let time = new Date();
while (new Date() - time < 2000) { }
console.log("3");
setTimeout(() => {
console.log("4")
}, 0);
let p = new Promise((resolve, reject) => {
console.log("5");
fn()
})
p.then(() => {
console.log("6")
}).catch(() => {
console.log("7")
});
console.log("8");
参考答案
答:1 3 5 8 7 2 4。
- 执行同步任务,首先打印出 1;
- 执行第一个定时器 s1,放进宏任务里;
- 执行 while 语句,由于需要等待 2s 后才能执行后面的代码,但是呢 s1 定时器等 while 语句执行完成后已经过了时间点,所以需要把 s1 放在宏任务的第一位;
- 继续执行同步任务,打印出 3;
- 把下一个定时器 s2 放在宏任务里,排在 s1 后面;
- 执行 promise 里的代码打印出 5;
- 因为 fn() 是一个不存在的函数,所以状态为 reject,放进微任务里;
- 执行同步任务,打印出 8;
- 执行异步代码,又因为微任务早于宏任务,所以先打印 7,最后执行宏任务,依次打印 2 4。
async/await
基本使用
async/await 是基于 Promise 的语法糖,使异步代码更易于编写和阅读。
async function fn() {
return await 123;
}
// 等价于
async function fn() {
return await Promise.resolve(123);
}
fn().then(res => console.log(res))
async/await与Promise的关系
- 执行 async 函数,返回的是 Promise 对象;
- await 相当于 Promise 的 then;
- try...catch可以捕获异常,替代了 Promise 的 catch。
async/await执行顺序
异步的本质还是回调函数。
async function async1() {
console.log("async1 start") // 2
await async2()
console.log("async1 end") // 5
}
async function async2() {
console.log("async2") // 3
}
console.log("start") // 1
async1()
console.log("end") // 4
async function async1() {
console.log("async1 start") // 2
await async2()
console.log("async1 among") // 5
await async3()
console.log("async1 end") // 7
}
async function async2() {
console.log("async2") // 3
}
async function async3() {
console.log("async3") // 6
}
console.log("start") // 1
async1()
console.log("end") // 4
for...of
for...of 常常用于异步的遍历。
function add(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num + num)
}, 1000)
})
}
let arr = [1, 2, 3]
arr.forEach(async (i) => {
let res = await add(i)
console.log(res)
})
上面代码中,将在 1s 后同时打印出 2 4 6,这不是我们想要的结果。我们想要每隔一秒再打印一个出来,这时候就需要用到 for...of。
function add(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num + num)
}, 1000)
})
}
let arr = [1, 2, 3]
!(async function () {
for (let i of arr) {
let res = await add(i)
console.log(res)
}
})()
EventLoop与DOM渲染
在文章开头我就讲解了 Event Loop 的执行过程,其实每当 Call Stack 空闲的时候,会先执行当前的微任务,然后尝试触发 DOM 渲染的,再然后去执行 Event Loop 机制。
因此,可以有如下总结:
- 宏任务:在 DOM 渲染后触发;
- 微任务:在 DOM 渲染前触发。
来看看下面这段代码:
<div id="demo"></div>
let $p1 = $('<p>how</p>')
let $p2 = $('<p>are</p>')
let $p3 = $('<p>you</p>')
$('#demo').append($p1).append($p2).append($p3)
console.log($('#demo').children().length);
alert("DOM结构已经更新,但未渲染")
当我们在浏览器打开后会发现,控制台打印出了 3,并且 alert 了,但是页面并没有渲染出内容。当我们点击确定后,便渲染出来了。
我们把上面代码更改一下:
let $p1 = $('<p>how</p>')
let $p2 = $('<p>are</p>')
let $p3 = $('<p>you</p>')
$('#demo').append($p1).append($p2).append($p3)
Promise.resolve().then(() => {
alert('DOM 渲染之前触发')
})
setTimeout(() => {
alert('DOM 渲染之后触发')
})
执行后,我们会发现,首先执行 alert('DOM 渲染之前触发')
,此时 DOM 并没有渲染,点击确定后,DOM 渲染并执行 alert('DOM 渲染之后触发')
。
最后在 Event Loop 的执行过程中,当同步代码执行完成后,Call Stack 空闲的时候,首先会去执行当前的微任务,然后去尝试触发 DOM 渲染,最后再去执行 Event Loop 机制,以此循环。
参考链接
阮一峰 - ECMAScript6入门 - Promise对象