欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

asap异步执行实现原理

程序员文章站 2022-07-02 09:22:59
目录 为什么分析asap asap概述 asap源码解析—Node版 参考 1.为什么分析asap 在之前的文章 "async和await是如何实现异步编程?" 中的 “浅谈Promise如何实现异步执行” 小节,提到了 Promise 异步执行是通过 "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.参考

【翻译】promises/a+规范

asap - high-priority task queue for node.js and browsers