为什么 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 相关的内容,欢迎关注。

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

掘金专栏 JavaScript 系列文章

  1. JavaScript之变量及作用域
  2. JavaScript之声明提升
  3. JavaScript之执行上下文
  4. JavaScript之变量对象
  5. JavaScript之原型与原型链
  6. JavaScript之作用域链
  7. JavaScript之闭包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值传递
  11. JavaScript之例题中彻底理解this
  12. JavaScript专题之模拟实现call和apply
  13. JavaScript专题之模拟实现bind
  14. JavaScript专题之模拟实现new
  15. JS专题之事件模型
  16. JS专题之事件循环
  17. JS专题之去抖函数
  18. JS专题之节流函数
  19. JS专题之函数柯里化
  20. JS专题之数组去重
  21. JS专题之深浅拷贝
  22. JS专题之数组展开
  23. JS专题之严格模式
  24. JS专题之memoization
  25. JS专题之垃圾回收
  26. JS专题之继承

JS异步编程之callback相关推荐

  1. Node.js 异步编程之 Callback介绍

    原文:http://www.jb51.net/article/63070.htm ------------------------------------- Node.js 基于 JavaScript ...

  2. JS异步编程之Generator

    前言 ES6 中提出一个叫生成器(Generator)的概念,执行生成器函数,会返回迭代器对象(Iterator),这个迭代器对象可以遍历函数内部的每一个状态. function* helloWorl ...

  3. 异步编程之Promise(2):探究原理

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  4. python3 sleep 并发_python异步编程之asyncio(百万并发)

    点击上方蓝字关注我们 目录 [python 异步编程之 asyncio(百万并发)] 一.asyncio 二.aiohttp 前言:python 由于 GIL(全局锁)的存在,不能发挥多核的优势,其性 ...

  5. pythonasyncio并发编程实战_python异步编程之asyncio(百万并发)

    [python异步编程之asyncio(百万并发)] 前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上 ...

  6. JS脚本编程之onchange事件

    Dynamics CRM 2011 编程系列(4):JS脚本编程之onchange事件 分类专栏: Dynamics CRM Microsoft Dynamics CRM 编程系列 版权 这节介绍on ...

  7. js模块化编程之CommonJS和AMD/CMD

    一.CommonJS 1.CommonJS API定义很多普通应用程序(主要指非浏览器的应用)使用的API,从而填补了这个空白.它的终极目标是提供一个类似Python,Ruby和Java标准库.这样的 ...

  8. ES6 异步编程之二:Promise

    异步回调的泥潭 异步回调是最直接的异步结果处理模式,将一个回调函数callback扔进异步处理函数中,当异步处理获得结果之后再调用这个回调函数就可以继续之后的处理,但是如果这个callback又是一个 ...

  9. dojo异步编程之dojo/Deferred

    dojo/Deferred介绍 dojo/Deferred是一个类,是Dojo中管理异步线程的基础. 简单来说,一个Deferred对象会等待一段时间再去执行指定的调用,直到某个特定的事件发生或者前一 ...

最新文章

  1. 数据分析师的职业规划之路
  2. 计算机硬件的基本组成
  3. java猜数字游戏应用程序_猜数字游戏的Java小程序
  4. BZOJ 2456 mode
  5. react项目如何按需加载antdDesign组件
  6. odoo pivot中去掉求和_一文读懂深度学习中的卷积运算与图像处理
  7. Mac下使用Homebrew安装Sphinx和MySQL
  8. OMG!这1010本书的书名都是什么鬼?
  9. java编译通过,运行却提示找不到或无法加载主类
  10. python 写csv scrapy_scrapy爬虫框架实例一,爬取自己博客
  11. C++命名空间和缺省参数的概念
  12. 市场经济下,一周休息2.5天难在落实
  13. vb mysql 实例_vb数据库编程实例-求VB连接数据库实例我想做一个VB连接数据库的简单实例,可以实现 爱问知识人...
  14. 乐高ev3python教程_入门篇丨使用EV3机器人,趣味学习Python编程语言~
  15. 制作一个简单HTML静态网页(HTML+CSS)
  16. 计算机显示器黑屏首先检查,计算机显示器黑屏的原因是什么?电脑显示器黑屏的解决方案...
  17. 建立远程桌面需要在对方服务器上做设置吗,SFB 项目经验-23-要远程登录,你需要具有通过远程桌面服务进行登录的权限...
  18. openstack核心组件-块存储 (Block Storage) : Cinder
  19. 【新手教程】51Sim-One Cloud 2.0如何构建一个V2X案例
  20. 直线电机模组在轨道交通(地铁轻轨磁浮)行业应用和发展优势

热门文章

  1. Filter(过滤器)
  2. jQuery 超屏加载
  3. 设计中涉及到的dip、dp、px、sp等单位说明
  4. enumeration学习
  5. python数据结构与算法:栈
  6. Spring Boot集成Swagger导入YApi@无界编程
  7. Linux下nginx支持.htaccess文件实现伪静态的方法!
  8. strcpy,memcpy和memmove区别
  9. libev源码解析——定时器原理
  10. Linux下getopt函数的使用