前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript 异步编程指南 — 事件与回调函数 Callback

JavaScript 异步编程指南 — 事件与回调函数 Callback

作者头像
一只图雀
发布2021-06-17 20:27:07
2.1K0
发布2021-06-17 20:27:07
举报
文章被收录于专栏:图雀社区图雀社区

这是一个系列文章,你可以关注公众号「五月君」订阅话题《JavaScript 异步编程指南》获取最新信息。

JavaScript 异步编程中回调是最常用和最基础的实现模式。回调就是函数,一般我们也会称它为 Callback,相信这对于 JavaScript 开发者不会陌生,而函数在 JavaScript 中属于一等公民,可以将函数传递给方法作为实参调用。

这种编程模式对于习惯同步思维的人来说很难理解,一般我们的大脑对事物的理解是同步的、线性的,在异步编程中它是一种相反的模式,你会看到代码的编写顺序与实际执行顺序并不是我们预期的,因为它们的编写与实际执行顺序也许没有什么直接的关系,特别是在处理一些复杂的业务场景时,掌握不好异步编程,通常也会写出糟糕的代码。

在笔者组建的技术交流群中,有时候大家提问一些问题,当看到一大堆 Callback 嵌套的代码时,感觉就很糟糕,顿时很难让人在有耐心去看它,这种模式它不会给予我们很友好的阅读体验,有时看到了我会说你先把代码书写逻辑整理下,也许问题就出在这里!

谈回调也少不了一个概念 “事件”,在使用 JavaScript 操作 DOM、网络请求或在 Node.js 中更多的是一种事件驱动的模型,由事件触发执行我们的回调。

定时器

例如,我们为 定时器 API 其传入一个函数,让其在将来某个时间之后执行。我们可以通过 setTimeout 或 setInterval 实现,前一个 setTimeout 是仅执行一次,后一个 setInterval 是间隔指定时间后重复执行。

这两个 API 在浏览器、Node.js 环境中使用都是一样的。

代码语言:javascript
复制
function fn() {
 // do something...
}
setTimeout(fn, 1000);
setInterval(fn, 1000);

网络事件

发起一个请求从另一端获取数据,这也是异步中很常见的一个操作,在客户端早期我们可以使用 XMLHttpRequest发起 HTTP 请求并异步处理服务器返回的响应。

代码语言:javascript
复制
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET', 'http://openapi.xxx.com/api');
httpRequest.send();
httpRequest.onreadystatechange = function() {
 if (httpRequest.readyState === XMLHttpRequest.DONE) {
      if (httpRequest.status === 200) {
        alert(httpRequest.responseText);
      } else {
        alert('There was a problem with the request.');
      }
    }
};

现在浏览器端有了一个新的 API fetch() 取代了复杂且名字容易误导人的 XMLHttpRequest,因为这个虽然名字带了 XML 但和 XML 没关系,fetch() API 完全基于 Promise 可以方便的让你编写代码从网络获取数据,简单看一下:

代码语言:javascript
复制
fetch('http://example.com/movies.json')
 .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

Node.js 中也定义了一些网络相关的 API,Node.js 提供的 HTTP/HTTPS 模块可以帮助我们在 Node.js 客户端向服务端请求数据

代码语言:javascript
复制
const http = require('http');
function sendRequest() {
  const req = http.request({
    method: 'GET',
    host: '127.0.0.1',
    port: 3010,
    path: '/api'
  }, res => {
    let data = '';
    res.on('data', chunk => data += chunk.toString());
    res.on('end', () => {
      console.log('response body: ', data);
    });
  });
  req.on('error', console.error);
  req.end();
}
sendRequest();

这种方式来写还是有点繁琐的,在实际的业务开发中我们使用一些功能完备的 HTTP 请求模块,例如 node-fetch、nodejs/undici、axios 等,这些工具都是可以基于 Promise 的形式。

Node.js 做为一个服务端启动,我们还可以使用 HTTP 模块,如下方式启动一个 Server:

代码语言:javascript
复制
const http = require('http');
http.createServer((req, res) => {
  req.on('data', chunk => {
  // TODO
 });
  req.on('end', () => res.end('ok!'))
  req.on('error', () => ...)
}).listen(3010);

客户端 DOM 事件与回调

客户端下的 JavaScript 我们可以获取指定的 DOM 元素,为特定类型的事件注册回调函数,当用户移动鼠标或移动触摸板、按下键盘时,浏览器会生成相应的事件并调用我们事先注册的回调函数,这些都是由事件驱动的。

下例,通过 addEventListener() 函数为事件注册回调函数。相对来说 DOM 事件在互相依赖、多级依赖嵌套的场景较少些,但是在 Node.js 里面你可能会遇到很多。

代码语言:javascript
复制
<button id="btn"> 点我哦 </button>
<script>
  const btn = document.getElementById('btn');

  // 单击时触发
  btn.addEventListener('click', event => console.log('click!'));

  // 鼠标移入触发
  btn.addEventListener('mouseover', event => console.log('mouseover!'));

  // 鼠标移出触发
  btn.addEventListener('mouseout', event => console.log('mouseout!'));
</script>

Node.js 中的事件与回调

Node.js 作为 JavaScript 的服务端运行时,大部分的 API 都是异步的,大家可能也听过 Node.js 比较擅长 I/O 密集型任务,这与它的单线程、基于事件驱动模型、异步 I/O是有关系的,它无需像多线程程序那样为每一个请求创建额外的线程、省掉了线程创建、销毁、上下文切换等开销。

它通过主循环加事件触发的方式执行程序,事件循环会不停地处理网络/文件 IO 事件,每一次的事件循环就是检查,检查是否有待处理的事件,如果有就取出事件及关联的回调函数,如果有传入 JavaScript 回调函数,传递到业务逻辑层执行,也许回调函数里还会在发起一次新的 I/O 请求,整个程序不断的通过事件循环调度执行。

也许你听过这样一句话:“它的优秀之处并非原创,它的原创之处并不优秀。” 异步 I/O 并非 Node.js 原创,但 Node.js 却是第一个成功的平台,Node.js 2009 年出现之前,JavaScript 在服务端近乎空白。例如,文件 API 在 Node.js 中默认就是异步的,也就是它的标准库 I/O 本身给你提供的就是非阻塞的,它没有任何的历史包袱。

谈到异步 I/O 必然少不了异步编程,早期我们的很多程序中都充斥着 Callback 风格的代码,包括 Node.js 提供的 API 大多数也是,大家都遵循一个默认的规则 “错误优先的回调函数”。

例如,下面 API 第一个参数为 err 如果有错误就是一个 Error 对象,否则就为 null,这也是一种默认的约定。

代码语言:javascript
复制
fs.readFile(filename, (err, file) => {
 // TODO
})

现在 Node.js 的一些系统模块已经为我们提供了一些工具可以方便的将 callback 转换为 Promise 的工具,或者文件模块我们可以通过 fs.promises 直接引入基于 Promise 版本的 API,这些编程方法我们会在后续章节 Promise 篇幅里讲。

一个糟糕的回调地狱例子

当我们在 Node.js 中有时需要处理一些复杂的业务场景,有些需要多级依赖,如果以 callback 形式很容易造成函数嵌套过深,例如下面示例很容易写出回调地狱、冗余的代码,这也是早期 Node.js 被人诟病比较多的地方。包括现在前段在群里仍然还有看到有些提问题的,写出类似于下面嵌套的代码,确实要改下了。

代码语言:javascript
复制
fs.readdir('/path/xxxx', (err, files) => {
  if (err) {
    // TODO...
  }
  files.forEach((filename, index) => {
    fs.lstat(filename, (err, stats) => {
      if (err) {
        // TODO...
      }
      if (stats.isFile()) {
        fs.readFile(filename, (err, file) => {
          // TODO
        })
      }
    })
  })
});

异步编程 Callback 的形式一个难点是上面说的容易出现回调地狱的例子,另外一方面是异常的处理很麻烦,在一些同步的代码中我们可以像下面示例这样使用 try/catch 捕获错误。

代码语言:javascript
复制
try {
 doSomething(...);
} catch(err) {
 // TODO
}

这种方式在一些异步方法面前显得无能为力,上面我们写的回调嵌套的示例,如果我们对 fs.readFile() 做 try/catch 捕获,当我们调用 fs.readFile 并为其注册回调函数这个步骤对应异步 I/O 中是提交请求,而 callback 函数会被存放起来,等到下一个事件循环到来 callback 才会被取出执行,这个时间是将来的某个时间点,而 try/catch 是同步的,捕获不到这个错误的。

下面因为我对一个 null 对象做了非法操作,这时程序会给我们报一个 TypeError: Cannot read property 'a' of null 错误,在 Java 中可以称它为空指针异常

类似于这样的一个错误如果没有被捕获到,在单进程的应用程序中必然会导致进程退出,无关语言

代码语言:javascript
复制
try {
 fs.readFile(filename, (err, file) => {
   const obj = null
    obj.a;
    // TODO
  })
} catch () {
 // TODO
}

有时候也会听大家说为什么我的 Node.js 程序老是崩溃?也有人说 Node.js 弱爆了(这个我曾经听过一个架构师这样说过...)如果程序这样写,就算你用的 Java 照样崩溃。

在延伸一点,Node.js 的 Process 对象为我们提供了两个事件可以用来捕获程序中出现的未捕获异常,方便程序优雅退出,这是笔者之前写的一篇文章,可以看看如何处理 Node.js 中出现的未捕获异常?

代码语言:javascript
复制
process.on('uncaughtException', fn);
process.on('unhandledRejection', fn);

总结

异步编程中 Callback 是比较早的模式,也是异步编程的基础,但是随着业务的发展、复杂度的上升,基于 Callback 的模式已经不能满足我们的需求了,就像我们的大脑对事物的思考,需要一种同步的、顺序的方式表达异步编程思想。

“办法总比困难多”,解决问题的方案还是很多的,目前的 JavaScript 中已有一些更高级、强大的异步编程模式,在本系列中会逐步的讲解。

代码语言:javascript
复制
● 字节跳动最爱考的前端面试题:CSS 基础● 字节跳动最爱考的前端面试题:JavaScript 基础● 字节跳动最爱考的前端面试题:计算机网络基础

·END·

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 图雀社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 定时器
  • 网络事件
  • 客户端 DOM 事件与回调
  • Node.js 中的事件与回调
  • 一个糟糕的回调地狱例子
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档