JS异步编程之callback

比特寻幽人
• 阅读 2758

为什么 JS 是单线程?

众所周知,Javascript 语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

而浏览器是多线程的,JS 线程就是其中一个:

  • 浏览器 GUI 渲染线程
  • JavaScript 引擎线程
  • 浏览器定时触发器线程
  • 浏览器事件触发线程
  • 浏览器 http 异步请求线程

浏览器线程知识中重要的一点是:

GUI渲染进程和 JavaScript 引擎进程是互斥的,因为如果这两个线程可以同时运行的话, JavaScript 的 DOM 操作将会扰乱渲染线程执行渲染前后的数据一致性。而且如果 DOM 一变化,界面就立刻重新渲染,效率必然很低

所以 JS 主线程执行任务时,浏览器渲染线程处于挂起状态。

同理,如果 JS 采用多线程同步的模型,那么如何保证同一时间修改了 DOM, 到底是哪个线程先生效呢?从操作系统调度多线程的上下文开销,到实际编程里的锁、线程同步等问题,都让开发变得比较困难。

所以 JS 最终采用了单线程的事件模型。

我之前的文章《JS专题之事件循环》也有讲过这块内容,欢迎翻阅。

一、同步与异步

单线程模式这种排队执行的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

那同步和异步的区别是什么?

我们想象一个很常见的场景:我们去面馆吃牛肉面,柜台人很多,前面在排队下单。

这个时候,同步就是,收银员收了你的钱,告诉你要在柜台站着等面煮好,煮好后,就端面开吃,后面的人也只能等前面的人面煮好了才能付款下单然后等着面煮好端走~

而异步就是,收银员收了你的钱,然后给了你一张小票,小票上有一个你的编号,收银员告诉你,可以去座位上,你的面一煮好,会大声叫你,你就来端面开吃。

我们可以看出,我们是过程的调用者,面馆是被调用者,牛肉面煮好,是我们想要的结果,同步是调用者需要主动地等待这个结果。异步是被动的等待结果,当被调用者有结果了,就会通过消息机制或者回调机制告诉调用者结果。

同步和异步关注的是消息通信机制,同步就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果, 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

以上:

  • 下单吃面是发起调用函数
  • 端面开吃的回调函数
  • 煮好的面是调用的结果,也是回调函数的参数

将例子抽象成伪代码:

orderNoodle("牛肉面", function(noodle) {
        // 端面
        getNoodle();
        // 吃面
        eatNoodle();
});

三、事件循环

关于事件循环如何执行异步代码可以翻阅前面的文章《JS专题之事件循环》,这里大概提一下。

如果遇到异步事件,JS 引擎会把事件函数压入执行调用栈,但浏览器识别到它是异步事件后,会将其弹出执行栈,当异步函数有返回结果后,JS 引擎将异步事件的回调函数放入事件队列中,如果执行调用栈为空,就将回调函数压入执行调用栈执行。

四、回调函数

在 JavaScript 中,函数 function 作为一等公民,使用上非常自由,无论调用它,或者作为参数,或者作为返回值都可以。

因为单线程异步的特点,后来在 JS 中,慢慢将函数的业务重点转移到了回调函数中。

function step1(cb) {
    console.log("step1");
    cb()
}

function step2(){
    console.log("step2");
}

step1(step2);  // step1  step2

代码会按先后顺序执行 step1, step2。

现在假设我们有这样的需求:请求文件1后,获取文件1 中的数据后请求文件2,获取文件 2 中的数据后,又请求文件三。

var fs = require("fs");

fs.readFile("./file1.json", function(err, data1) {
    fs.readFile("./file2.json", function (err, data2) {
        fs.readFile("./file3.json", function(err, data3) {
            
        })
    })
})

五、回调函数的问题

由第四节可以看出,回调函数的写法存在很多问题。

  1. 回调地狱(洋葱模型)

当多个异步事务多级依赖时,回调函数会形成多级的嵌套,被花括号一层层包括,代码变成
金字塔型结构,也被称为回调地狱和洋葱模型。

在回调地狱的情况下,代码逻辑的梳理,流程的控制,代码封装维护,错误处理都变得越来越困难。

  1. 异常处理

try...catch 是被设计成捕获当前执行环境的异常,意思是只能捕获同步代码里面的异常,异步调用里面的异常无法捕获。

function readFile(fileName) {
    setTimeout(function () {
      throw new Error("类型错误");
    }, 1000);
}
try {
    readFile('./file1.json');
} catch (e) {
    // 如果异步事件出错,打印不出来错误信息
    console.log('err', e);
}

在 nodejs 对回调函数采用 error first 的思想,回调函数的第一个参数保留给一个错误error对象,如果有错误发生,错误将通过第一个参数err返回。

原因是一个有回调函数的函数,执行分两段,第一段执行完之后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文已经无法捕捉,只能当做参数,传入第二阶段。

fs.readFile('/etc/passwd', 'utf8', function (err, data) {
    if(err) {
        console.log(err)
        return;
    }
});

总结

回调函数是 JS 异步编程中的基石,但同时也存在很多问题,不太适合人类自然语言的线性思维习惯。

接下来几篇文章,我将梳理 JS 中异步编程中的历史演进中 Promise, generator, async&await 相关的内容,欢迎关注。

JS异步编程之callback

点赞
收藏
评论区
推荐文章
凯特林 凯特林
4年前
React - Fiber原理
浏览器渲染屏幕刷新率(FPS)浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿绘制一次的称为一帧,平均每帧16.6ms帧每个帧的开头包括样式计算、布局和绘制js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的如果某个js任务执行时间过长,浏览器会推迟渲染,每
Wesley13 Wesley13
3年前
java 面试知识点笔记(十三)多线程与并发
java线程池,利用Exceutors创建不同的线程池满足不同场景需求:1.newSingleThreadExecutor() 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。2.
菜园前端 菜园前端
2年前
什么是JavaScript同步模式?
原文链接:什么是同步模式?大部分单线程任务都会排队执行任务,这就称为同步模式(Synchronous)。同步模式执行中,只涉及到调用栈(Callstack)。现实生活举例就像验核酸一样,我们要排队一个个去验(按顺序排队),当只有一条通道(也就是单线程)时,
菜园前端 菜园前端
2年前
什么是 JavaScript 单线程?
原文链接:什么是JavaScript单线程?单线程是指当存在多个任务时候,所有任务都必须排队并且按照顺序执行。这里就会有同学问为什么不是多线程?假设是多线程,同时执行一些任务,某个任务进行了该dom的修改,而某个任务进行了该dom的删除,这样就会出现一些问
Stella981 Stella981
3年前
JavaScript:再谈Tasks和Microtasks
JavaScript是单线程,也就是说JS的堆栈中只允许有一类任务在执行,不可以同时执行多类任务。在读js文件时,所有的同步任务是一条task,当然了,每一条task都是一个队列,按顺序执行。而如果在中途遇到了setTimeout这种异步任务,就会将它挂起,放到任务队列中去执行,等执行完毕后,如果有callback,就把callback推入到Tasks中去,
Easter79 Easter79
3年前
Spring注解@Scheduled 多线程异步执行
一、前言:Spring定时任务@Schedule的使用方式,默认是单线程同步执行的,启动过程是一个单线程同步启动过程,一旦中途被阻塞,会导致整个启动过程阻塞,其余的定时任务都不会启动。二、@Schedule注解多线程的实现:多个定时任务的执行,通过使用@Async注解来实现多线程异步调用。@Scheduled(
Stella981 Stella981
3年前
JavaScript同步、异步及事件循环
同步、异步JS是单线程的,每次只能做一件事情。像以下这种情况,代码会按顺序执行,这个就叫同步。console.log(1);console.log(2);console.log(3);以下代码会输出2、3、1,像这种不按顺序执行的,或者说代码执行中间有时间间隙的,叫异步。setTimeout((
Wesley13 Wesley13
3年前
JS异步的底层原理:单线程加事件队列
异步的底层原理:单线程事件队列。js的代码执行时单线程的,所谓单线程:就是js代码时从上到下按顺序依次执行的,一次只能做一件事情。事件队列可以看作一个容器,这个容器存储着js的回调函数,只有js代码执行结束后,才会去事件队列中调用这些回调函数。例:1//以下的代码先执行for循环,再输出sum值,然后输出正常代码执行,最后
Stella981 Stella981
3年前
JavaScript 事件循环机制
javascript是一门单线程的非阻塞的脚本语言。单线程意味着javascript在执行代码的任何时候,都只有一个主线程来处理所有的任务。那么javascript引擎是如何实现这一点的呢?因为事件循环(eventloop)。先上图:!event_loop(https://oscimg.oschina.net/oscnet/up6
Stella981 Stella981
3年前
ES6 Promise
Promisepromise是异步编程的一种解决方案1什么是异步?异步模式,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。"异步模式"非常重要。在浏
Stella981 Stella981
3年前
JavaScript 异步编程
❝掌握JavaScript主流的异步任务处理(本篇文章内容输出来源:《拉钩教育大前端训练营》参阅《你不知道的JavaScript中卷》异步章节)❞JavaScrip采用单线程模式工作的原因,需要进行DOM操作,如果多个线程同时修改DOM浏览器无法知道以哪个线程为主。JavaScirpt分为:同步模式、异步模式同步