asap异步执行实现原理
目录
- 为什么分析asap
- asap概述
- asap源码解析—node版
- 参考
1.为什么分析asap
在之前的文章 中的 “浅谈promise如何实现异步执行” 小节,提到了 promise 异步执行是通过 这个库来实现的。所以为了进一步深入 promise 异步执行的原理,深入分析一下 asap 是有必要的。
2.asap概述
asap 是 as soon as possible 的简称,在 node 和浏览器环境下,能将回调函数以高优先级任务来执行(下一个事件循环之前),即把任务放在微任务队列中执行。
宏任务(macro-task)和微任务(micro-task)表示异步任务的两种分类。在挂起任务时,js 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
用法:
asap(function () { // ... });
3.asap源码解析—node版
asap 源码库中包含了支持node和浏览器的两个版本,这里主要进行分析node版。
主要包含两个源码文件:
这两个文件分别导出了 asap 和 rawasap 这两个方法,而 asap 可以看作是对 rawasap 的进一步封装,通过缓存的 domain(可以捕捉处理 try catch 无法捕捉的异常,针对异步代码的异常处理)和 try/finally 实现了即使某个任务抛出异常也可以恢复任务栈的继续执行,另外也做了一点缓存优化(具体见源码)。
因此这里主要分析 raw.js 里面的代码即可:
1.首先是对外导出的 rawasap 方法
var queue = []; var flushing = false; function rawasap(task) { if (!queue.length) { requestflush(); flushing = true; } queue[queue.length] = task; }
源码解析:如果任务栈 queue 为空,则触发 requestflush 方法,并将 flushing 标志为 true,并且始终会将要执行的 task 添加到任务栈 queue 的末尾。这里需要注意的是由于 requestflush 中是异步去触发任务栈的执行的,所以即使queue[queue.length] = task
在 requestflush 调用之后执行,也能保证在任务栈 queue 真正执行前,任务 task 已经被添加到了任务栈 queue 的末尾。(如果任务栈 queue 不为空,说明 requestflush 已经触发了,此时任务栈正在被循环依次执行,执行完毕会清空任务栈)
2.其次是异步触发 flush 方法执行的 requestflush 方法
var domain; var hassetimmediate = typeof setimmediate === "function"; // 设置为 rawasap 的属性,方便在任务执行异常时再次触发 requestflush rawasap.requestflush = requestflush; function requestflush() { // 确保 flushing 未绑定到任何域 var parentdomain = process.domain; if (parentdomain) { if (!domain) { // 惰性加载执行 domain 模块 domain = require("domain"); } domain.active = process.domain = null; } if (flushing && hassetimmediate) { setimmediate(flush); } else { process.nexttick(flush); } if (parentdomain) { domain.active = process.domain = parentdomain; } }
源码解析:核心代码其实就一句:setimmediate(flush),通过 setimmediate 异步执行 flush 方法。而判断 parentdomain 以及设置和恢复 domain 都只是为了当前的 flush 方法不绑定任何域执行。而这里还有一个 hassetimmediate 判断,是为了做兼容降级处理,如果不存在 setimmediate 方法,则使用 process.nexttick 方法触发异步执行。但使用 process.nexttick 方法有一个缺陷,就是它不能够处理递归。
3.最后是执行任务栈的 flush 方法
// 下一个任务在任务队列中执行的位置 var index = 0; var capacity = 1024; function flush() { while (index < queue.length) { var currentindex = index; // 在调用任务之前先设置下一个任务的索引,可以确保再次触发 flush 方法时,跳过异常任务 index = index + 1; queue[currentindex].call(); // 防止内存泄露 if (index > capacity) { for (var scan = 0, newlength = queue.length - index; scan < newlength; scan++) { queue[scan] = queue[scan + index]; } queue.length -= index; index = 0; } } queue.length = 0; index = 0; flushing = false; }
源码解析:通过 while 循环依次去执行任务栈 queue 中的每一个任务,这里需要注意一点,index + 1
表示下一个要执行的任务下标,而其放在 queue[currentindex].call()
之前,是为了保证当当前任务执行发生异常了,再次触发 requestflush 方法时,能够跳过发生异常的任务,从下一个任务开始执行。而判断 if (index > capacity)
是为了防止内存泄露,当任务栈 queue 的长度超过了指定的阈值 capacity 时,对任务栈 queue 中的任务进行移动,将所有剩余的未执行的任务置前,并重置任务栈 queue 的长度。当所有任务执行完毕后,重置任务栈以及相应状态。
4.总结
rawasap 方法是通过 setimmediate 或 process.nexttick 来实现异步执行的任务栈,而 asap 方法是对 rawasap 方法的进一步封装,通过缓存的 domain 和 try/finally 实现了即使某个任务抛出异常也可以恢复任务栈的继续执行(再次调用rawasap.requestflush)。
4.参考
上一篇: Flutter 实现网易云音乐字幕