异步操作
异步操作
进程与线程
进程是 CPU 资源分配的最小单位,是能拥有资源和独立运行的最小单位。
线程是 CPU 调度的最小单位,是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程。
单线程模型
JavaScript 只在一个线程上运行。JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。
注意:JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker
标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。新标准并没有改变 JavaScript 单线程的本质。
同步任务和异步任务
Javascript 有一个主线程(main thread
)和调用栈(call-stack
,执行栈),所有的任务都会被放到调用栈等待主线程执行。引擎提供一个任务队列(task queue
),队列中是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。)
- 主线程会执行所有的同步任务。
- 等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。
- 等到执行完,下一个异步任务再进入主线程开始执行。
- 一旦任务队列清空,程序就结束执行。
调用栈(call-stack
,执行栈)是一种后进先出的数据结构。当函数被调用时,会被添加到栈的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。栈可存放的函数是有限制的,一旦存放过多的函数且没有得到释放的话,就会出现爆栈的问题。
JavaScript 单线程中的任务广义可分为:
- 同步任务:在主线程上排队执行的任务,在调用栈中按照顺序等待主线程依次执行.
- 异步任务:不进入主线程,而进入任务队列的任务。在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
JavaScript 单线程中的任务可细分为:
- MacroTask(宏任务):
script
(整体代码)、setTimeout
、setInterval
、I/O
操作、UI Rendering
(UI 渲染)、requestAnimationFrame
、setImmediate
。 - MicroTask(微任务):
Promise
、MutationObserver
、Process.nextTick
(Node独有)
异步操作的模式
回调函数
function asyncFn(callback) { callback() } function callbackFn() {} // asyncFn 异步执行完成之后,回到执行 callbackFn 函数 asyncFn(callbackFn)
事件监听
// 监听 done 事件 asyncTriggerFn.on('done', fn) function asyncTriggerFn( setTimeout(function () { // 表示执行完成后,立即触发 donw 事件,从而开始执行 fn 函数 asyncTriggerFn.trigger('done') }) )
发布/订阅
// 订阅 done $.subscribe('done', fn) function asyncPublishFn( setTimeout(function () { // asyncPublishFn 异步执行完成之后,发布 done,从而开始执行 fn 函数 $.publish('done') }) ) // 取消订阅 done $.unsubscribe('done', fn)
异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。
function async(arg, callback) {
console.log("参数为 " + arg + " , 1秒后返回结果")
setTimeout(function () {
callback(arg * 2)
}, 1000)
}
function final(value) {
console.log("完成: ", value)
}
async(1, function (value) {
async(2, function (value) {
async(3, function (value) {
async(4, function (value) {
async(5, function (value) {
async(6, final)
})
})
})
})
})
// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成: 12
串行执行:控制异步任务,一个任务完成以后,再执行另一个。
var items = [1, 2, 3, 4, 5, 6] var results = [] function async(arg, callback) { console.log("参数为 " + arg + " , 1秒后返回结果") setTimeout(function () { callback(arg * 2) }, 1000) } function final(value) { console.log("完成: ", value) } function series(item) { if (item) { async(item, function (result) { results.push(result) return series(items.shift()) }) } else { return final(results[results.length - 1]) } } series(items.shift())
并行执行:所有异步任务同时执行,等到全部完成以后,才执行
final
函数。var items = [1, 2, 3, 4, 5, 6] var results = [] function async(arg, callback) { console.log("参数为 " + arg + " , 1秒后返回结果") setTimeout(function () { callback(arg * 2) }, 1000) } function final(value) { console.log("完成: ", value) } items.forEach(function (item) { async(item, function (result) { results.push(result) if (results.length === items.length) { final(results[results.length - 1]) } }) })
并行和串行结合:每次最多只能并行执行 n 个异步任务,避免了过分占用系统资源。
var items = [1, 2, 3, 4, 5, 6] var results = [] var running = 0 var limit = 2 function async(arg, callback) { console.log("参数为 " + arg + " , 1秒后返回结果") setTimeout(function () { callback(arg * 2) }, 1000) } function final(value) { console.log("完成: ", value) } function launcher() { while (running < limit && items.length > 0) { var item = items.shift() async(item, function (result) { results.push(result) running-- if (items.length > 0) { launcher() } else if (running == 0) { final(results) } }) running++ } } launcher()