深入理解JavaScript的单线程异步

基础概念

进程和线程

进程——是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。对操作系统来说,一个任务就是一个进程,即一个运行着的程序就对应了一个进程。比如打开一个浏览器就是启动一个浏览器进程,打开两个word就是启动了两个word进程。对于单核CPU来说,同一时间只能运行一个进程。那么,我们为什么还能够一边听音乐一边读word文件呢?这是它采用任务交替执行的方式,一会运行音乐进程,一会运行word进程,因为CPU的执行速度非常快,所以我们一般感受不到这种切换。 

线程——也被称为轻量级进程,指运行中的程序的调度单位。线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程,线程不拥有系统资源,只有运行必须的一些数据结构,它与父进程的其他线程共享该进程所拥有的全部资源。一般线程有就绪,运行和阻塞三种状态。多线程是指程序中包含多个执行流,即在一个程序中可是同时运行多个不同的线程来执行不同的任务,比如我们可以打开音乐软件之后一边听音乐一边查看歌词,这就是一个进程下的两个线程。而单线程就只有一个执行流。但事实上单线程也可以实现多任务,它用并发的方式,即一会执行这个任务,一会执行那个任务,CPU的切换速度非常快,以至于我们觉得好像是多线程在工作,但实际上这种单线程并发在某个时间段内只能执行一件事情。

并行和并发

并行——是指程序的运行状态,在同一个时间有几件事情并行在处理。由于一个进程/线程在同一时间只能处理一件事情,所以并行需要多个进程/线程在同一时间执行多个事情。

并发——是指程序的设计结构,在同一时间内多件事情能被交替的处理。重点是,在某个时间段内只能够有一件事情被执行。比如单核CPU能实现多任务运行的过程就是并发的。

同步和异步

两者产生需要有一个前提——是否有多个任务或事件发生,只有满足了这一前提,才有了同步和异步的概念。

同步——如果有多个任务或事件发生,这些任务或事件必须逐个的进行。一个事件或者任务的执行会导致整个流程的暂停等待。

异步——如果有多个任务或事件发生,这些事件是独立的,在等待某事件的过程中,线程可以执行其他任务,不需要等待这一事件完成后再工作,而是把等待某件事情的工作放到另一线程内,等其完成后返回回调函数再返回到原来的线程中继续执行。因此,要实现异步必须是多线程的。

再补充一点,所谓同步,就是在发出一个功能调用时,再没有得到结果之前,该调用就不用返回。按照这个定义,其实绝大部分函数都是同步调用。但是一般而言,我们在说同步和异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。

阻塞和非阻塞

两者也有一个前提——某个任务或者事件正在执行。它们是程序在等待消息时的状态。

阻塞——当某个任务或事件在执行过程中,它发出一个请求操作,但是由于这个请求操作需要的条件不被满足,那么当前线程会被挂起,一直到条件满足。

非阻塞——当某个任务或事件在执行过程中,它发出一个请求操作,如果这个请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直等待。即在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立即返回。

JavaScript的单线程异步

JavaScript为什么是单线程?

JavaScript的单线程是与它的用途有关系的。作为浏览器脚本语言,JavaScript的主要用途是与用户互动及操作DOM,这决定了它只能是单线程。假设它是多线程的,多个线程同时操作DOM,一个增加了某个DOM节点,一个删除了某个DOM节点,那么浏览器应该怎么办?所以,为了避免这个复杂性的问题,JavaScript就是单线程的。

补充:为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个新标准并没有改变JavaScript单线程的本质。

JavaScript为什么是异步的?

前面提到过,如果有多个任务或事件发生,同步就是指这些任务必须逐个的执行,前一个任务结束,才会执行后一个任务。如果一个任务耗时很长,后一个任务就必须等待着。如果排队是因为计算量很大,CPU忙不过来倒也算了,但是很多时候CPU是闲着的,因为I/O设备很慢(比如Ajax操作从网络读取数据),这样极大的浪费了时间。因此,JavaScript语言的设计者意识到,JS线程完全可以不管I/O设备,挂起处于等待中的任务,先运行排在 后面的任务。等到I/O设备返回了结果,再回过头把挂起的任务执行下去。那么根据需求来说,采用异步方式是不错的选择。那么又有疑惑了,我们前面说JS是单线程的,那么它又怎么样实现异步呢?

再此我需要强调一点,单线程和异步确实是相冲突的,单线程不能实现异步。为什么呢?

我们想一想单线程的概念,虽然它能够实现多任务,但是是靠单线程并发的方式去执行的,事实上它在某个时间段内只能执行一个任务,其他任务暂停执行。而异步是使用多线程并行来执行多任务的。

JavaScript的宿主环境浏览器

值得注意的是,我们一直说JS是单线程的,这实际上是JS引擎是单线程的,但JS的运行的宿主环境浏览器时多线程的。下面我们来了解一下浏览器

目前的主流浏览器的内核都是多线程的。一个浏览器通常有以下几个常驻线程:

  • JS引擎线程:解析和执行JS代码
  • 渲染引擎线程:负责页面的渲染
  • http请求线程:处理http请求,如Ajax
  • 定时器触发线程:处理定时事件,如setTimeout和setInterval
  • 事件触发线程:处理DOM事件

到这里,我们就能够理解为什么说JavaScript既是单线程又是异步的了,因为它还是依赖浏览器这个多线程的宿主环境,把一部分任务交由其他浏览器线程去处理,就实现了异步。至于这个异步执行的机制,下面再详细说。

JavaScript的运行机制

JavaScript的任务类型

JS的执行任务分为两种,同步任务和异步任务。这里称JS引擎线程为主线程。

同步任务指的是在主线程上排队执行的任务,只有前一个任务完成,才会执行下一个任务。

异步任务指的是,不进入主线程,而进入其他线程的任务,异步任务必须有回调函数。如Ajax请求任务交给http请求线程去执行。一旦异步任务有了结果,就会在“任务队列”中放置一个事件。

任务队列

队列,即先进先出的一种数据结构,JS中的任务队列,与异步任务的执行有着紧密的联系,只要异步任务有了运行结果,就会在任务队列中放置一个事件。简单的说,任务队列就是用来按顺序放置与异步任务运行结果有关的事件。

此文中不再对同步任务机制进行详解,主要介绍JS的异步任务的运行机制

异步执行机制——循环机制(Event Loop)

  • 当JS引擎线程(主线程)执行JS代码时,遇到同步任务时,会将要执行的同步任务放置到执行栈中,假如是调用一个函数,就会将这个函数入栈,执行完毕后再弹出执行栈;当遇到异步任务时,主线程会发起一个异步请求,异步代码被挂起,比如当遇到Ajax请求时,主线程就会向http请求线程发送一个异步请求;
  • 相应的工作线程接收请求,同时主线程继续执行后面的代码,同时相应的其他线程执行异步任务。一旦有了运行结果,就在“任务队列”中放置一个事件,这个事件就是指“回调函数”。
  • 一旦主线程中所有同步任务执行完毕,即执行栈为空的时候,系统就会读取任务队列中的事件。那些在任务队列中的事件结束等待状态,进入执行栈,开始执行;
  • 主线程不断重复上一步。

主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称事件循环(Event Loop)

setTimeout

在前面我们说过,当主线程中的同步任务执行完后,就会立即去任务队列里读取事件,这些事件按顺序进入执行栈里执行。但是对于有定时功能的代码来说,当主线程中的同步任务执行完之后,不会立即去读取任务队列里的事件,而是会等待设置的时间之后才去读取。

function foo(){
    console.log(1);
}

function bar(){
    console.log(2);
}

foo();

setTimeout(function cb(){
    console.log(3);
},1000);

bar();

上面的例子中,先执行foo( ),输出1,然后主线程遇到异步代码setTimeout,发起异步请求,再继续执行bar( ),输出2,然后主线程中同步任务执行完毕,执行栈为空,然后等待1000毫秒后去从任务队列里读取回调函数事件,输出3。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值