阅读 147

NodeJs深入浅出之旅:异步I/O (上)🐋

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

异步I/O

在前端,最经典的异步就是Ajax

文章介绍:《高性能 JavaScriptの六 -- 老生常谈 Ajax》

Node是首个大规模将异步I/O应用在应用层上的平台,力求在单线程上将资源分配的更高效

为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中Web Workers的子进程,子进程可以通过工作进程高效的利用CPU和I/O

异步IO调用.png

Node异步I/O基本要素:

  • 事件循环:
  • 观察者:
  • 请求对象
  • I/O线程池

事件循环是一个典型的生产者/消费者模型。 异步I/O、网络请求是事件生产者,为Node提供不同类型的事件,事件被传递到对应的观察者那里,事件循环从观察者那取出事件并处理

官方介绍:nodejs.cn/learn/the-n…

异步流程

异步IO流程.png

node中使用import: blog.csdn.net/goutinga/ar…

测试代码:

import { nextTick } from 'process';

module.exports = function (app) { 
    nextTick(() => console.log(1));  // 优先级第二
    setImmediate(() => console.log(2));  // 优先级最低
    Promise.resolve().then(() => console.log(3)); // 优先级第三
    queueMicrotask(() => console.log(4));  // 优先级第四
    setTimeout(() => console.log(6), 0);  // 优先级第五
    console.log(5)  // 优先级最高
}
复制代码

输出的顺序:5 1 3 4 6 2

这里可以发现,nextTick中回调函数的执行优先级最高,而setImmediate的执行优先级最低。

原因在于事件循环对观察者的检查是有先后顺序的, nextTick属于idle观察者, setImmediate属于check观察者。 idle观察者先于I/O观察者, I/O观察者先于check观察者


异步编程 🐟

高阶函数

在通常的语言中,函数的参数只接受基本的数据类型或是对象引用,返回值也只是基本数据类型和对象引用。

基本函数:

function foo(x) { 
    return x;
}
复制代码

高阶函数是可以把函数作为参数,或是将函数作为返回值

高阶函数:

function foo(x) {
    return function () {
        return x;
    };
}
复制代码

偏函数

偏函数用法指创建一个调用另外一个部分(参数或变量已经预置的函数)的函数用法

举个粒子, 定义判断类别的方法:

var toString = Object.prototype.toString;

var isString = function(obj) {
    return toString.call(obj) == '[object String]';
}
var isFunction = function(obj) {
    return toString.call(obj) == '[object Function]';
}
复制代码

如果需要重复定义一些相似的函数,这样代码就会冗余。 为了解决重复定义的问题,可以创建一个新函数:

var isType = function(type) {
    return function(obj) {
        return toString.call(obj) == '[object ' + type + ']';
    }
}

var isString = isType('String');
var isFunction = isType('Function');
复制代码

这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数




异常处理

在普通异常处理当中,通常使用try/catch/final语句块进行异常捕获。

但是这在异步编程中并不一定使用。 异步I/O实现主要包涵在两个阶段:1、提交请求 2、处理结果。

这两个阶段中间有事件循环的调度,所以彼此不关联

graph LR;
    提交请求 --> 事件循环 --> 处理结果

Node在处理异常上形成了一种约定,将异常作为回调函数第一个实参传回,如果为控制,则表明异步调用没有异常抛出。

JavaScript的 try…catch 机制不能用来截获异步方法产生的错误。新手的常见错误之一是试图在错误优先回调函数中使用 throw

参考:《错误优先的回调》

错误例子:

const fs = require('fs');

try {
  fs.readFile('/some/file/that/does-not-exist', (err, data) => {
    // 错误的假设:在这里抛出错误。
    if (err) {
      throw err;
    }
  });
} catch (err) {
  // 这里不会截获回调函数中的 throw。
  console.error(err);
}
复制代码

会出现报错:

trycatch异步报错.png


正确例子:

function errorFirstCallback(err, data) {
    if (err) {
      console.error('出错', err);
      return;
    }
    console.log(data);
}
fs.readFile('./src/b.txt', errorFirstCallback)
复制代码




异步编程解决方案 🐳

异步编程主要解决方案有三种

  • 事件发布/订阅模式
  • Promise/Deferred模式
  • 流程控制库

事件发布/订阅模式

事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式。

Node自身提供的events事件触发器模块是发布/订阅模式的一个简单实现。

所有触发事件的对象都是 EventEmitter 类的实例。 这些对象暴露了 eventEmitter.on() 函数,允许将一个或多个函数绑定到对象触发的命名事件。 通常,事件名称是驼峰式字符串,但也可以使用任何有效的 JavaScript 属性键。

EventEmitter 对象触发事件时,所有绑定到该特定事件的函数都会被同步地调用。 被调用的监听器返回的任何值都将被忽略和丢弃。

以下示例展示了使用单个监听器的简单的 EventEmitter 实例。 eventEmitter.on() 方法用于注册监听器,eventEmitter.emit() 方法用于触发事件

简单例子

const { EventEmitter, errorMonitor } = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
// 订阅
myEmitter.on('event1', (val) => {
  console.log(val);
});
// 发布
myEmitter.emit('event1', 'hello world!');
复制代码

结果:在订阅中输出 hello world!

订阅函数就是一个高阶函数的应用, 事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又被称为事件监听器

通过emit()发布事件后,消息回立即传递给当前事件的所有监听器执行,监听器可以添加和删除。

在Node中, emit()调用多半是伴随事件循环而异步触发的。

监听器过多警告

注意: 如果一个事件添加了超过10个监听器,将会得到一条警告。 并且事件相关的监听器过多,可能存在过多占用CPU的情景。

for (let i = 0; i < 11; i++) {
    myEmitter.on('event1', (val) => {
        console.log(i);
    });
}
// 发布
myEmitter.emit('event1', 'hello world!');
复制代码

监听器过多警告.png

如果想要去除警告,调用setMaxListeners(0);可以去除限制

myEmitter.setMaxListeners(0); 添加在for循环前即可

文章分类
前端
文章标签