大家好,我是林一一,异步编程在 JS 中是无法避免的,也是面试必问的。本文使用通俗易懂的语言解析异步编程中的原理,开始阅读吧😁
思维导图
一、定时器
定时器:设定一个定时器,到了设定时间,浏览器会把对应的方法执行。每一个定时器执行后都会有一个编号返回,每个定时器编号不一样。
1. 设定定时器
- setTimeout([function], [interval])
function 都是在到达设定时间后才执行。且执行一次
let count = 1
let timer = setTimeout(function(){
count++
console.log(count) // 2
}, 1000)
console.log(timer) // 1
- setInterval([function], [interval])
在设定时间内执行,不主动停止的情况下一直执行。
let count = 1
let timer = setInterval(function(){
count++
console.log(count) // 2
}, 1000)
console.log(timer) // 1
/*
* 2
* 3
* 4
* ...
*/
2. 清除定时器
clearTimeout/clearInterval两者都可以清除上面的两种定时器。
- 如何清除定时器?
只需要定时器的返回值编号清除即可。
let count = 1
let timer = setInterval(function(){
count++
console.log(count)
// count == 3 ? clearTimeout(timer) : null
count == 3 ? clearInterval(timer) : null
}, 1000)
二、异步编程的原理
先来看一个小例子
let a = 0
setTimeout(() =>{
console.log('a', ++a)
}, 0)
console.log(a)
/* 输出
* 0
* 1
*/
上面的例子中,
setTimeout是异步的,浏览器会将异步的代码加入到任务队列中,等到同步的代码执行完成后才执行异步的代码
1. 同步
JS 是单线程的,代码至上而下执行时遇到同步的代码需要先执行完才可以进行下一步任务。比如循环等
2. 异步
所有需要等待的任务都是异步的。遇到异步代码时,不需要等待而是直接异步任务放入任务队列,等到后面的任务完成后,才会返回来执行没有完成异步的代码。比如事件绑定,所有定时器,ajax 的异步处理,部分回调函数,浏览器的渲染过程等等。
let a = 0
setTimeout(() =>{
console.log('a', ++a)
}, 0)
console.log(a)
while(true){
}
上面的代码死循环了,即使定时器的时间到了也不会执行。因为同步的代码没有执行完一步就不会执行。
三、promise
1.基本概念
Promise只是一个管理异步编程的类,本身是同步。Promise有三个状态pending/fulfilled/rejected,三个状态只有两个状态出现要么成功要么失败。new Promise()时必须要传入回调函数executor,否则报错。其中回调函数中有两个参数resolve, reject,这两个参数可不写。
pending:初始化状态,开始执行异步的任务,只要执行 new,new Promise(()=>{}),promise 的状态就会变成pendingfulfilled:成功状态,执行resolve()。rejected:失败状态, 执行rejected()。 先看一个小栗子。
new Promise(()=> {
setTimeout(()=> {
console.log(1)
}, 0)
console.log(2)
}).then()
console.log(3)
/* 输出
* 2
* 3
* 1
*/
创建一个新的
Promise的实例也就是new这个过程中会把Promise 中的函数先执行(不清楚new创建实例的过程中发生了什么可以看看这篇 面试 | 你不得不懂得 JS 原型和原型链)。函数体内有异步操作的仍会加入任务队列,等到同步执行完成后才执行异步任务,比如函数体内的setTimeout 函数。所以输出的结果就是2,3,1。
再来看一个小栗子
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
promise 的状态只能改变一次,最后的状态不是
fulfilled就是rejected。也就是说同一个promise中的resolve()/ reject()只能执行一个且一次。
2. promise 是怎么管理异步的
promise参数的回调函数体内接收两个参数resolve/ reject,这两个参数可以作为两个回调函数。
resolve():是异步操作执行成功后执行,promise 的状态变成了fulfilled,可以提供返回值,在.then()中第一个参数接收reject():异步操作执行失败后执行,promise 的状态变成了rejected,可以提供返回值,在.then()中第二个参数接收resolve()和reject()中只能传递一个参数。promise 的状态发生改变后不会再变化。resolve() 和 reject() 是异步操作,执行这两个方法时,会先执行resolve/reject下面的同步代码,等到主任务为空时,再去调用resolve/reject把存放的方法执行。Promise对象上有私有属性Promise.resolve()/ Promise.reject()等。 举一个没什么意义的小栗子
new Promise((resolve, reject)=> {
setTimeout(()=> {
console.log('林')
resolve('ok')
// reject('fail')
console.log('一一')
}, 0)
}).then( res => {
console.log('status:', res)
}, res => {
console.log('status:', res)
})
// 林
// 一一
// ok
上面的栗子直接看出
resolve/reject是异步的。
3. promise.then(onfulfilled, onrejected) / promise.catch()
promise.then(onfulfilled, onrejected)
promise.then()方法中有两个参数,分别对应着 promise 的两种不同的状态,fulfilled, rejected。对应的状态执行对应的方法。promise.then()中的参数是函数,如果传递的不是函数就会造成值穿透,也就是resolve()/reject()的返回值会.then()中接收。promise.then()能够链式的调用,能够链式调用的原因不是.then()方法中有return this,而是每一个.then()方法中都会返回一个新的Promise实例。
promise.catch()
promise.catch()是promise.then()第二个参数的简便写法,也就是用来捕获reject()执行后的rejected状态。.catch()也可以实现链式调用,原因和.then()方法一样都是返回了一个新的promise。
.then()/.catch() 中的返回值都不能是 promise自己本身的实例,因为会造成死循环
热身题
1.举一个小栗子
Promise.resolve(1)
.then((res) => {
console.log(res)
return 2
}).catch(err => {
console.log(err)
}).then( res => {
console.log(res)
})
// 1 2
最终输出
1 2,
2. 举一个值穿透的小栗子
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 等价于
// Promise.resolve(1)
// .then(console.log)
3. then()/.catch() 内不能返回自己本身的promise 实例,举一个栗子
let pro = Promise.resolve()
.then(() => {
console.log('promise', pro)
return pro
})
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
创建的
promise实例是pro,返回值就不能是pro, 否则会造成死循环。
4. 关于 Promise.catch()
先看一道面试题
new Promise((resolve, reject) => {
reject(1)
}).catch(() => {
console.log(2)
// throw 'err'
}).then(() => console.log(3), (v) => console.log(v))
// 输出: 2 3
不知你答对了没
- catch 确实是 .then 第二个参数的语法糖。值得一提的是在
.catch内部如果没有抛出错误或一个错误的 promise 的reject(),那么都认为 .catch 返回的结果值是resolve()的都将显示为成功。输出第二个输出是 3。
5.并行 Promise.all([promise1, promise2...])
Promise.all()中需要等待参数中所有promise的状态都成功才执行回调的.then(),如果有一个是失败的那么就执行.catch()。接收的参数是一个包含promise实例的数组,.all()这个方法的返回值也是一个新的promise实例。
- 全部执行成功回调
.then()接收到的就是一个数组 - 如果有执行失败的
promise状态,回调.catch中就会捕获到执行失败的promise。 - 执行失败的 promise 会让 promise.all 方法立即停止执行。
var p1 = Promise.resolve(1)
var p2 = Promise.resolve(2)
var p3 = Promise.resolve(3)
let pro = Promise.all([p1, p2, p3])
.then(res => {
console.log(res) // [1, 2, 3]
})
.catch( err => {
console.log(err)
})
console.log(pro) // Promise {<pending>}
全部执行成功,那么
.then()获取到的值就是resolve()的返回值数组。
6. Promise.race()
.race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取一个最快执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
- 接收的是一组数组,只获取一个最快执行完成
resolve()/rejected()的返回值,返回值不是一个数组
var p1 = new Promise(function(resolve, reject) {
setTimeout(reject, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
});
两个都完成,但 p2 更快
7. Promise.finally()
.finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。
- 要注意的是finally函数的参数只是一个回调函数,这个回调函数不接收任何的参数,比如下面的 f 打印的就是 undefined,不会接收 then/catch return 过来的值
let promise = new Promise((resolve, reject) => {
reject('error')
}).then( res => {
console.log('then', res)
return res
}).catch( err => {
console.log('catch', err) // `catch error`
return err
}).finally( f => {
console.log(f) // undefined
})
热身题
热身题1
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
// 1, 2, 4, 3
因为
resolve()是异步的,promise.then也是异步的,在没有获取到resolve()的fulfilled状态时.then()不会执行。
热身题2
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
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)
})
/* 输出
* once
* success 1002
* success 1002
*/
promise.then()即使有多个,但是resolve()/reject()后的调用时同时执行的。所以同时输出了success 1002
热身题3
const p = function(){
return new Promise((resolve, reject) =>{
const p1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(1)
},0)
resolve(2)
})
p1.then((res) =>{
console.log(res)
})
console.log(3)
resolve(4)
})
}
p().then(res => {
console.log(res)
})
console.log('end')
// 3, end, 2, 4
这里只需要注意是
resolve(2)先进入队列,resolve(4)·才进入的异步队列。同时resolve(1)` 抛出的结果不会在被执行,因为 Promise 状态已经改变。
三、async 和 await
先来看一个栗子
function fn(){
return new Promise((resolve, reject) => {
setTimeout( () => {
Math.random() < 0.5 ? resolve('resolve 001') : reject('reject 002')
}, 0)
})
}
async function get() {
let res = await fn()
console.log(res)
console.log(1212)
}
get()
1. async
async和await是 ES7 中增加来对promise操作的方法,是 ES7 系列提供的语法糖,await不能单独使用一定要结合async来使用。async会返回一个promise对象,async函数调用不会造成代码的阻塞
2. await
await是用来等待获取一个promise的resolve/reject的执行结果,像上面的let res = await fn()是先把fn()执行后,来获取resolve/reject返回的结果,不过await后面也可以不跟着一个promise,但是这样写就没有意义了。await 或 await fn()这个操作不是同步的,而是异步的。await下面的代码不会执行,而是移入到任务队列等待区,等到主栈中的其他任务完成且fn()中的promise将结果返回,await下面的代码才可以重新回到主栈中执行。await可以使promise的操作更加像同步的代码。- 如果
await等待的Promise返回值如果是rejected/peding,await下面的代码都不会执行,reject()返回的代表已经报错了。记住 await 10 等价于 await Promise.resolve(10) 举一个小栗子,说明 async/await 是语法糖
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
// 上面的代码等价于 ==>
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end')
})
}
思考
热身1,await 是同步吗?,求输出的结果
console.log(1)
function fn(){
return new Promise((resolve, reject) => {
console.log(5)
resolve('resolve 001')
console.log(6)
})
}
async function get() {
console.log(2)
let res = await fn()
console.log(res)
console.log(3)
}
get()
console.log(4)
//1, 2, 5, 6, 4, resolve 001, 3
await是异步的同时会让出线程,fn()执行后线程开始让出,那么await的下面的代码不会立即执行会先到主栈的等待区,主栈中console.log(4)执行后,再回来执行await处的代码。
2. 热身2,求输出结果
console.log(1)
async function get() {
console.log(2)
let res = await 200
console.log(res)
console.log(3)
}
get()
console.log(4)
// 1, 2, 4, 200, 3
根据上面一题
await异步代码的原因,可以容易的分析出答案。同时说明await后面也可以跟着一个非promise的实例。
3. 热身,求输出结果
async function fn(){
await fo() // Promise.resolve(fo()) ==> undefined
console.log(1)
}
async function fo(){
}
fn()
// 1
输出结果是 1 因为
fo()有返回值,且返回值不是reject()
4. 热身,求输出结果
async function async1() {
console.log('async1 start')
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success')
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
// srcipt start, async1 start, promise1, srcipt end,
上面没有输出
async1 success是因为 await 返回的Promise实例还处于 pending 状态,没有结果返回所以下面的代码不会执行。async1的返回值是一个 Promise,也不会打印async1 end
思考题(输出以chrome浏览器为准)
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
}).then(function () {
console.log('promise3');
});
console.log('script end');
// script start async1 start async2 start async2 promise promise1 script end promise2 promise3
// async1 end
// setTimeout
你可能会对
async1 end的输出位置感到困惑。那我们再来看一道题。
- 对比和第一题的差别在于 async2 函数前面没有了
async
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
}).then(function () {
console.log('promise3');
});
console.log('script end');
// script start async1 start async2 start async2 promise promise1 script end
// async1 end
// promise2 promise3
// setTimeout
async1 end现在按照预期先进入了微任务的队列,再到promise2 promise3, 在 ecma 的规范中如果添加了async关键字的函数在这里是(async2)同时包含return Promise的函数,那么async1中的await的异步任务就相当于跟在所有的x.then().then()的后面,所以在思考1中async1 end在promise2 promise3后面。认证如下:
async function async1() {
await async2();
console.log('async1 end');
}
async function async2() {
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
}).then(function () {
console.log('promise3');
}).then(function () {
console.log('promise4');
});
//async2 promise
//promise1
//promise2
//promise3
//async1 end
//promise4
所以这里的
async1 end就在promise3和promise4中间输出
练习(字节)
async function async1() {
console.log('async1 start');
await async2()
console.log('async end');
}
async function async2() {
return new Promise((resolve, reject) => {
console.log('async2 start');
resolve()
}).then(res => {
console.log('async2 end');
})
}
async1()
new Promise(resolve => {
console.log('Promise');
resolve()
}).then(res => {
console.log('Promise end');
})
console.log('script end');
// async1 start async2 start Promise script end async2 end Promise end async end
async end也出现在了的x.then().then()后面才执行。
四、经典面试题
1. promise 的优缺点/为什么使用 promise?
- 优点:promise 可以解决回调地狱,promise 大大增强了嵌套函数的可读性和可维护性,
- 缺点:无法取消 Promise,错误需要通过回调函数来捕获;如果不设置回调函数,Promise 内部抛出的错误,不会反映到外部;当处于 pending(等待)状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成
2. setTimeout、Promise、Async/Await 的区别
- setTimeout: setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行
- Promise: Promise 本身是同步的立即执行函数,当在 executor 中执行 resolve或者 reject 的时候,此时是异步操作,会先执行 then/catch 等,当主栈完成时,才会去调用 resolve/reject 方法中存放的方法。
- async: async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
3. 实现一个 sleep 函数,比如 sleep(1000) 意味着等待 1000 毫秒。
function sleep1(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
})
}
sleep1(1000).then(() => console.log("sleep1"));
4. Promise 构造函数是同步执行还是异步执行,那么 then 方法呢
Promise是同步的,执行new promise(callback)时回调函数callback就会被立即执行,then()方法是异步的。
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
1243,promise 构造函数是同步执行的,then 方法是异步执行的
5. 介绍下 Promise.all 使用、原理实现及错误处理
const p = Promise.all([p1, p2, p3]);
Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise resolve 方法,将参数转为 Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
5. 介绍下 Promise.all, Promise.race() 的区别
看上面的介绍
Promise 的各种 api 实现会在下一篇文章中实现。给个期待吧😂
五、参考
BAT前端经典面试问题:史上最最最详细的手写Promise教程
六、结束
感谢阅读到这里,如果着篇文章能对你有一点启发或帮助的话欢迎 github star, 我是林一一,下次见。