如何理解JS异步编程
JavaScript语言执行环境是“单线程”(单线程,就是指一次只能完成一件任务,如果有多个任务就必须排队等候,前面一个任务完成,再执行后面一个任务)。这种“单线程”模式执行效率较低,任务耗时长。 为了解决这个问题,提出了“异步模式”(异步模式,是指后一个任务不等前一个任务执行完就执行,每个任务有一个或多个回调函数)。 异步模式使得JavaScript在处理事务时非常高效,但也带来很多问题,如异常处理困难、嵌套过深。
基础知识
单线程与多线程
进程和线程
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
提示:
- 不同进程之间也可以通信,不过代价较大
- 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)
JavaScript 的设计就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),决定了它是一门单线程语言。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
- GUI渲染线程
const t1 = new Date()
setTimeout(() => {
const t3 = new Date()
console.log('t3 - t1 =', t3 - t1)
}, 100)
let t2 = new Date()
while (t2 - t1 < 200) {
t2 = new Date()
}
console.log('end')
调用栈(call stack)
调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
JS 执行机制
console.log('script start')
setTimeout(() => {
console.log('timer 1 over')
}, 1000)
console.log('script end')
// script start
// script end
// timer 1 over
消息队列与事件循环
消息队列:
也称为任务队列(task queue),是一个先进先出的队列,它里面存放着各种消息,即异步操作的回调函数,异步操作会将相关回调添加到任务队列中,而不同的异步操作添加到任务队列的时机也不同,如onclick,setTimeout,ajax处理的方式都不同,这些异步操作都是由浏览器内核的不同模块来执行的:
- onclick由浏览器内核的DOM Binding模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中;
- setTimeout会由浏览器内核的timer模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中;
- ajax会由浏览器内核的network模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中;
事件循环(Event Loop):
-
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
- 浏览器的Event Loop是在html5的规范中明确定义。
-
NodeJS的Event Loop是基于libuv实现的。
-
libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve(() => {console.log('pr3')}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
宏任务和微任务
-
宏任务/宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
-
微任务/微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
Object.observeMutationObserverqueueMicroTask
console.log('1')
new Promise((resolve, reject) => {
console.log('2')
resolve()
})
.then(() => {
console.log('3')
return new Promise((resolve, reject) => {
console.log('4')
resolve()
}).then(() => {
console.log('5')
})
})
.then(() => {
console.log('6')
})
console.log('7')
// script here
// 1 promise
// end here
// 1 promise then
// 2 promise
// 2 promise then
// another promise
浏览器中的 Event Loop
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
小结:
- 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;
- 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
实现异步的四种方式
回调、Promise、generator、async
// 用回调函数实现
const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
callback()
}, timer)
}
const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step)
})
})
}
step()
// 用 promise 实现
const task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
}
const step = () => {
task(3000, 'red')
.then(() => {
task(2000, 'green')
})
.then(() => {
task(1000, 'yellow')
})
.then(step)
}
step()
// 用 async 实现
const step = async () => {
await task(3000, 'red')
await task(2000, 'green')
await task(1000, 'yellow')
step()
}
step()