JS异步编程之callback
栏目: JavaScript · 发布时间: 5年前
内容简介:众所周知,Javascript 语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。而浏览器是多线程的,JS 线程就是其中一个:
为什么 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) { }) }) })
五、回调函数的问题
由第四节可以看出,回调函数的写法存在很多问题。
- 回调地狱(洋葱模型)
当多个异步事务多级依赖时,回调函数会形成多级的嵌套,被花括号一层层包括,代码变成
金字塔型结构,也被称为回调地狱和洋葱模型。
在回调地狱的情况下,代码逻辑的梳理,流程的控制,代码封装维护,错误处理都变得越来越困难。
- 异常处理
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 相关的内容,欢迎关注。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人类2.0
皮埃罗∙斯加鲁菲(Piero Scaruffi) / 闫景立、牛金霞 / 中信出版集团股份有限公司 / 2017-2-1 / CNY 68.00
《人类2.0:在硅谷探索科技未来》从在众多新技术中选择了他认为最有潜力塑造科技乃至人类未来的新技术进行详述,其中涉及大数据、物联网、人工智能、纳米科技、虚拟现实、生物技术、社交媒体、区块链、太空探索和3D打印。皮埃罗用一名硅谷工程师的严谨和一名历史文化学者的哲学视角,不仅在书中勾勒出这些新技术的未来演变方向和面貌,还对它们对社会和人性的影响进行了深入思考。 为了补充和佐证其观点,《人类2.0......一起来看看 《人类2.0》 这本书的介绍吧!