同步、异步
同步
同步调用函数的时候会马上执行,同时等待返回结果,即进入阻塞状态,得到返回结果后再进行下一步执行
异步
异步会将操作和结果在时间上分隔开,在执行完操作后,并不会等待返回结果,继续执行同步代码,等到执行结果返回后,再来进行返回结果的处理,也就是说异步执行是非阻塞的
let timeOut = setTimeout(() => {
console.log('一秒后打印')
clearTimeout(timeOut)
}, 1000)
console.log('马上打印')
// 马上打印
// 一秒后打印
异步操作是否都是采用回调函数的形式?
let n = {
counter: {
i: 1
}
}
console.log(n)
n.counter.i++
// 浏览器 { counter: {i: 2} }
// 浏览器推迟了 console.log(n) i/o 操作
// node { counter: {i: 1} }
为什么使用异步
因为 JavaScript 是单线程的,在执行的时候无法直接像其他语言通过额外的进程、线程、协程来实现。
JavaScript 异步的实现的原理
将一些操作交给其他线程处理,然后采用事件循环
的机制来处理返回的结果
let eventLoop = []; // 事件队列,先进先出
let event;
while (true) {
// 一次 tick
if(eventLoop.length > 0){
// 从队列取出函数
event = eventLoop.shift();
try {
event();
} catch (err) {
throw new Error(err)
}
}
}
以 AJAX 请求为例,当发送一个 AJAX 请求时,浏览器会将请求任务分派给网络线程来进行处理,当请求处理完成后,会将回调的函数放到事件队列中,让后在进行事件轮询的时候便会被执行
setTimeout
和setInterval
也是一样的道理,当执行setTimeout
时浏览器并不会直接把回调函数放到事件队列中,而是交给定时器线程处理,当处理完成后才会放到事件队列中,等待轮询被执行
这样就可能存在一个隐性的问题,如果事件队列中已经有其他事件,那这个回调就会排队等待,因此setTimeout
和setInterval
的精度并不高,它只能确保回调函数不会在规定的时间之前执行,无法保证精确的时间间隔
事件队列先进先出的弊端
由于事件队列按照先进先出的原则,如果队列较长,那么排在后面的事件即使较为紧急也需要等待前面的函数执行完才能执行
为此 JavaScript 采用多个事件队列对回调事件的优先级进行区分
setTimeout(console.log.bind(null,1),0)
Promise.resolve().then(console.log.bind(null,2))
// 2
// 1
Promise
为微任务队列 setTimeout
为宏任务队列
优先级
ProcessingInstruction.nextTick(Node.js) > MutationObserver(浏览器)/Promise.then(catch?finnally) > setImmediate(IE) > setTimeout/setInterval/request/Animation/Frame > 其他 i/o操作、浏览器 DOM 事件
处理异步
由于回调函数的可读性差,在编写代码时尽量使用 Promise 对象的形式,同时还可以配合 ES6 的 async/await 关键字进行使用,也可以转换成 Observable 对象,很多第三方库的异步函数,也采用 返回 Promise 对象的方式
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
})
});
let p2 = Promise.resolve(2);
([p1,p2].reduce(async (lastPromise, i) => {
return await lastPromise + await i
}, Promise.resolve(0))).then(result => {
console.log(result)
})
// 3
异步并行
Promise.all([p1,...,p2])
调用 Promise.all
函数会返回一个新的 Promise 实例
该实例在参数内所有 promise 都完成时回调完成(resolve),如果有一个失败,直接返回失败(reject)
当执行异步函数具有较强的一致性可以使用它,比如更新一个较大的表单数据,多次请求分别更新不同数据
Promise.allSettled([p1,...,p2])
调用Promise.allSettled
函数会返回一个新的 Promise 实例,该实例会在所有给定的 promise 执行完成后返回一个对象数组,每个对象对应一个结果
适用于需要同时并发执行多个异步函数,结果互相独立的情况
Promise.race([p1,...,p2])
调用Promise.race
函数会返回一个新的 Promise 实例,一旦某个 Promise 完成,立即返回执行结果
异常捕获
Promise 不能通过 try/catch 进行异常捕获,一旦内部发生异常就会自动进入失败状态(rejected)
try {
new Promise((resolve,reject) => {
throw new Error('test')
})
} catch(err) {
console.log('try/catch', err) // 捕获不到
}
try {
new Promise((resolve,reject) => {
throw new Error('test')
}).catch((err) => {
console.log('Promise/catch', err)
// Promise/catch Error: test
})
} catch(err) {
console.log('try/catch', err)
}
Promise 局限性
-
立即执行
当一个 Promise 实例被创建时,内部代码就会立刻执行,而且无法停止,比如无法取消超时或消耗性能的异步调用,造成资源浪费
-
单次执行
Promise 处理问题都是一次性的,因为每个 Promise 实例只能 resolve 或 reject 一次,无法很好应对持续响应场景,例如上传文件获取进度时,默认采用的是通过事件监听的方式实现的