Async Hooks 是 Node8 新出来的特性,提供了一些 API 用于跟踪 NodeJs 中的异步资源的生命周期,属于 NodeJs 内置模块,可以直接引用。

const async_hooks = require('async_hooks');

这是一个很少使用的模块,为什么会有这个模块呢?

我们都知道,JavaScript在设计之初就是一门单线程语言,这和他的设计初衷有关,最初的JavaScript仅仅是用来进行页面的表单校验,在低网速时代降低用户等待服务器响应的时间成本。随着Web前端技术的发展,虽然前端功能越来越强大,越来越被重视,但是单线程似乎也没有什么解决不了的问题,相比较而言多线程似乎更加的复杂,所以单线程依旧被沿用至今。

既然JavaScript是单线程,但是在日常开发中总是会有一些比较耗时的任务,比如说定时器,再比如说如今已经标准化的Ajax,JavaScript为了解决这些问题,将自身分为了BOM,DOM,ECMAScript,BOM会帮我们解决这些耗时的任务,称之为异步任务。

正因为浏览器的BOM帮我们处理了异步任务,所以大部分的程序员对异步任务除了会用几乎一无所知,比如同时有多少异步任务在队列中?异步是否拥堵等,我们都是没有办法直接获得相关信息的,很多情况下,底层确实也不需要我们关注相关的信息,但如果我们在某些情况下想要相关信息的时候,NodeJS提供了一个Experimental的API供我们使用,也就是async_hooks。为什么是NodeJS呢,因为只有在Node中定时器,http这些异步模块,才是开发者可以控制的,浏览器中的BOM是不被开发者控制的,除非浏览器提供对应的API。

async_hooks规则

async_hooks约定每一个函数都会提供一个上下文,我们称之为async scope,每一个async scope中都有一个 asyncId, 是当前async scope的标志,同一个的async scope中asyncId必然相同。

这在多个异步任务并行的时候,asyncId可以使我们可以很好的区分要监听的是哪一个异步任务。

asyncId是一个自增的不重复的正整数,程序的第一个asyncId必然是1。

async scope通俗点来说就是一个不能中断的同步任务,只要是不能中断的,无论多长的代码都共用一个asyncId,但如果中间是可以中断的,比如是回调,比如中间有await,都会创建一个新的异步上下文,也会有一个新的asyncId。

每一个async scope中都有一个triggerAsyncId表示当前函数是由那个async scope触发生成的;

通过 asyncId 和 triggerAsyncId 我们可以很方便的追踪整个异步的调用关系及链路。

async_hooks.executionAsyncId()用于获取asyncId,可以看到全局的asyncId是1。

async_hooks.triggerAsyncId()用于获取triggerAsyncId,目前值为0。

const async_hooks = require('async_hooks');
console.log('asyncId:', async_hooks.executionAsyncId()); // asyncId: 1
console.log('triggerAsyncId:', async_hooks.triggerAsyncId()); // triggerAsyncId: 0

我们这里使用fs.open打开一个文件,可以发现fs.open的asyncId是7,而fs.open的triggerAsyncId变成了1,这是因为fs.open是由全局调用触发的,全局的asyncId是1。

const async_hooks = require('async_hooks');
console.log('asyncId:', async_hooks.executionAsyncId()); // asyncId: 1
console.log('triggerAsyncId:', async_hooks.triggerAsyncId()); // triggerAsyncId: 0
const fs = require('fs');
fs.open('./test.js', 'r', (err, fd) => {
    console.log('fs.open.asyncId:', async_hooks.executionAsyncId()); // 7
    console.log('fs.open.triggerAsyncId:', async_hooks.triggerAsyncId()); // 1
});

异步函数的生命周期

当然实际应用中的async_hooks并不是这样使用的,他正确的用法是在所有异步任务创建、执行前、执行后、销毁后,触发回调,所有回调会传入asyncId。

我们可以使用async_hooks.createHook来创建一个异步资源的钩子,这个钩子接收一个对象作为参数来注册一些关于异步资源生命周期中可能发生事件的回调函数。每当异步资源被创建/执行/销毁时这些钩子函数会被触发。

const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
})

目前 createHook 函数可以接受五类 Hook Callbacks 如下:

1.init(asyncId, type, triggerAsyncId, resource)

  • init 回调函数一般在异步资源初始化的时候被触发。
  • asyncId: 每一个异步资源都会生成一个唯一性标志
  • type: 异步资源的类型,一般都是资源的构造函数的名字。

FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject

  • triggerAsyncId: 表示触发当前异步资源被创建的对应的 async scope 的 asyncId
  • resource: 代表被初始化的异步资源对象

我们可以通过 async_hooks.createHook 函数来注册关于每个异步资源在生命周期中发生的 init/before/after/destory/promiseResolve 等相关事件的监听函数;
同一个 async scope 可能会被调用及执行多次,不管执行多少次,其 asyncId 必然相同,通过监听函数,我们很方便追踪其执行的次数及时间及上线文关系;

2.before(asyncId)

before函数一般在 asyncId 对应的异步资源操作完成后准备执行回调前被调用,before回调函数可能被执行多次,由其被回调的次数来决定,使用时这里需要注意。

3.after(asyncId)

after回调函数一般在异步资源执行完回调函数后会立即被调用,如果在执行回调函数的过程中发生未捕获的异常,after 事件会在触发 “uncaughtException” 事件后被调用。

4.destroy(asyncId)

当asyncId对应的异步资源被销毁时调用,有些异步资源的销毁要依赖垃圾回收机制,所以有些情况下由于内存泄漏的原因,destory事件可能永远不会被触发。

5.promiseResolve(asyncId)

当 Promise 构造器中的 resovle 函数被执行时,promiseResolve 事件被触发。有些情况下,有些 resolve 函数是被隐式执行的,比如 .then 函数会返回一个新的 Promise,这个时候也会被调用。

const async_hooks = require('async_hooks');

// 获取当前执行上下文的 asyncId
const eid = async_hooks.executionAsyncId();

// 获取触发当前函数的 asyncId
const tid = async_hooks.triggerAsyncId();

// 创建新的AsyncHook实例。所有这些回调都是可选的
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// 需要显示声明 才能执行
asyncHook.enable();

// 禁止监听新的异步事件。
asyncHook.disable();

function init(asyncId, type, triggerAsyncId, resource) { }

function before(asyncId) { }

function after(asyncId) { }

function destroy(asyncId) { }

function promiseResolve(asyncId) { }

Promise

promise是比较特殊的一种情况,如果足够细心init方法中的type中你就会发现其中并没有PROMISE。如果仅使用ah.executionAsyncId()来获取Promise的的asyncId的话,是不能取得正确的ID的,只有在添加了实际的hook只后,async_hooks才会给Promise的回调创建asyncId。

换句话说,由于V8对于获取 asyncId 的执行成本比较高,所以默认情况下,我们是不给 Promise 分配新的 asyncId。
也就是说默认情况下,我们使用promises或者 async/await 时是获取不到当前上下文正确的asyncId和triggerId。不过没关系,我们可以通过执行async_hooks.createHook(callbacks).enable()函数强制开启对Promise分配asyncId。

const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
})
asyncHook.enable();

Promise.resolve(123).then(() => {
  console.log(`asyncId ${async_hooks.executionAsyncId()} triggerId ${async_hooks.triggerAsyncId()}`);
});

另外Promise只会触发init和promiseResolve钩子事件函数,而before和after事件的钩子函数只会在Promise的链式调用时被触发,也就是说只有在.then/.catch函数中生成的Promise时才会被触发。

new Promise(resolve => {
    resolve(123);
}).then(data => {
    console.log(data);
})

可以发现,上面的存在两个Promise,第一个是new实例化创建的,第二个是then创建的(不明白的可以查看之前的Promise源码文章)。

这里的顺序是执行new Promise的时候会调用自身的init函数,然后在执行resolve的时候调用promiseResolve函数。接着在then方法中执行第二个Promise的init函数,然后执行第二个Promise的before,promiseResovle,after函数。

异常处理

如果注册的async-hook回调函数中发生异常,那么服务将打印错误日志并立即退出,同时所有de 监听器将被移除,同时会触发 ‘exit' 事件退出程序。

之所以会立即退出进程,是因为如果这些async-hook 函数运行不稳定,下一个相同事件被触发时很可能又抛出异常,这些函数主要就是为了监听异步事件的,如果不稳定应该及时发现并进行更正。

在异步钩子回调中打印日志

由于 console.log 函数也是一个异步调用,如果我们在 async-hook 函数中再调用 console.log 那么将再次触发相应的 hook 事件,造成死循环调用,所以我们在 async-hook 函数中必须使用同步打印日志方式来跟踪,可以使用 fs.writeSync 函数:

const fs = require('fs');
const util = require('util');

function debug(...args) {
  fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' });
}

[参考文献-AsyncHooks] (https://nodejs.org/dist/latest-v15.x/docs/api/async_hooks.html)

到此这篇关于Node8中AsyncHooks异步生命周期的文章就介绍到这了,更多相关Node AsyncHooks异步生命周期内容请搜索程序员的世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持程序员的世界!

Node8中AsyncHooks异步生命周期的更多相关文章

  1. node.js常用内置模块一

    在使用内模块的时候需要先将所需的内置模块进行引入、OS模块在nodejs中OS模块提供了与操作系统相关的属性和方法// 导入OS内置模块,必须先进行导入,否则无法使用 const os = require("os") // 根据操作系统生成对应的换行符 console.log(o......

  2. three.js cannon.js物理引擎之Heightfield

    今天郭先生说一说cannon.js物理引擎之Heightfield高度场,学过场论的朋友都知道物理学中把某个物理量在空间的一个区域内的分布称为场,高度场就是与高度相关的场,而cannon.js物理引擎的Heightfield的高度就是关于两个变量的函数,可以表达为HEIGHT(i,j)。当然知不知道......

  3. nodejs中的文件系统

    、目录简介nodejs中的文件系统模块Promise版本的fs文件描述符fs.stat文件状态信息fs的文件读写fs的文件夹操作path操作简介nodejs使用了异步IO来提升服务端的处理效率。而IO中一个非常重要的方面就是文件IO。今天我们会详细介绍一下nodejs中的文件系统和IO操作。node......

  4. 一文秒懂nodejs中的异步编程

    文章目录 简介同步异步和阻塞非阻塞javascript中的回调回调函数的错误处理回调地狱 ES6中的Promise什么是PromisePromise的特点Promise的优点Promise的缺点Promise的用法Promise的执行顺序 async和awaitasync的执行顺序async的特点 ......

  5. 比较node.js和Deno

    前言如果你一直关注 Web 开发领域,那么最近可能已经听到了很多关于 Deno 的信息——一种新的JavaScript运行时,它可能也会被认为是 Node.js的继承者。但是这意味着什么,我们需要“下一个 Node.js” 吗?什么是 Deno?要了解发生了什么,我们首先需要看一下 Deno 到底是......

  6. nodejs的调试debug

    目录简介开启nodejs的调试调试的安全性使用WebStorm进行nodejs调试使用Chrome devTools进行调试使用node-inspect来进行调试其他的debug客户端简介对于开发者来说,在开发应用程序的过程中,往往为了开发方便和解决bug需要借助于编程语言的调试功能。一般来说我们需......

  7. 在nodejs中创建child process

    目录简介child process异步创建进程同步创建进程在nodejs中创建child process简介nodejs的main event loop是单线程的,nodejs本身也维护着Worker Pool用来处理一些耗时的操作,我们还可以通过使用nodejs提供的worker_threads来......

  8. 在nodejs中创建cluster

    目录简介cluster集群cluster详解cluster中的eventcluster中的方法cluster中的属性cluster中的worker总结在nodejs中创建cluster简介在前面的文章中,我们讲到了可以通过worker_threads来创建新的线程,可以使用child_process......

  9. nodejs的错误处理过程记录

    本文以连接错误ECONNREFUSED为例,看看nodejs对错误处理的过程。 假设我们有以下代码1. const net = require('net'); 2. net.connect({port: 9999})如果本机上没有监听9999端口,那么我们会得到以下输出。1. events.......

  10. typescript编写微信小程序创建项目的方法

    创建项目在微信开发者工具创建项目,在语言中选择 TypeScript改造项目编辑 package.json 文件,修改 miniprogram-api-typings 和 typescript 版本{"name": "miniprogram-ts-quickstart&......

随机推荐

  1. python中re模块的使用(正则表达式)

    一、什么是正则表达式?正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。二、正则表达式的匹配规则1.表......

  2. python opencv常用图形绘制方法(线段、矩形、圆形、椭圆、文本)

    最近学了下 python opencv,分享下使用 opencv 在图片上绘制常用图形的方法。案例中实现了在图片中添加线段、圆形、矩形、椭圆形以及添加文字的方法,使用 opencv2 实现的。实现方法1)画线段 cv.line在图片中绘制一段直线# 绘制线段# 参数1:图片# 参数2:起点# 参数3......

  3. @SpringBootApplication注解的使用

    一、前言大部分的配置都可以用Java类+注解来代替,而在SpringBoot项目中见的最多的莫过于@SpringBootApplication注解了,它在每个SpringBoot的启动类上都有标注。这个注解对SpringBoot的启动和自动配置到底有什么样的影响呢?本文将为各位大佬解析它的源码,揭开......

  4. CSS使用 background 创造各种美妙的背景

    本文属于 CSS 绘图技巧其中一篇,系列文章:在 CSS 中使用三角函数绘制曲线图形及展示动画CSS奇思妙想 -- 使用 CSS 创造艺术将介绍一些利用 CSS 中的 background、mix-blend-mode、mask 及一些相关属性,制作一些稍微复杂、酷炫的背景。通过本文,你将会了解到 ......

  5. python pandas合并Sheet,处理列乱序和出现Unnamed列的解决

    使用python中的pandas,xlrd,openpyxl库完成合并excel中指定sheet的操作# -*- coding: UTF-8 -*- import xlrdimport pandas as pdfrom pandas import DataFramefrom openpyxl imp......

  6. JS数组处理汇总

    join()方法:将数组中所有元素通过指定分隔符连接成一个字符串举例:myArr.join('-') // 用'-'符号拼接concat()方法:将两个数组或多个数组合并成一个数组举例:myArr.concat(arr1, arr2, ..., arrN)注意:该方法不会改变现有的数组,所以可以和空......

  7. postman接口自动化测试之利用node.js和xmysql连接、操作数据库

    一、背景使用postman进行接口自动化测试时,除了要验证接口的返回,有时候还要同时验证数据库的数据,或者将接口返回的数据与数据库的数据做对比,检验数据的正确性。有的时候还需要在执行自动化case之前,造一些测试数据,或者在跑完自动化之后,删除测试数据。所以,我们需要在postman里连接并操作数据......

  8. 深入学习SpringCloud之SpringCloud简介

    Spring Cloud是什么?SpringCloud官网:http://spring.ioSpring Cloud是一个一站式的开发分布式系统的框架,为开发者提供了一系列的构建分布式系统的工具集。Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(比如:配置管理,服务发......

  9. 全面了解Vue3的 reactive 和相关函数

    Vue3的 reactive 怎么用,原理是什么,官网上和reactive相关的那些函数又都是做什么用处的?这里会一一解答。ES6的ProxyProxy 是 ES6 提供的一个可以拦截对象基础操作的代理。因为 reactive 采用 Proxy 代理的方式,实现引用类型的响应性,所以我们先看看 Pr......

  10. Java中的clone方法实例详解

    Java中对象创建clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。那么在java语言中,有几种方式可以创建对象呢?1 使用new操作符创建一个对象2 使用clone方法复制......