进程和线程
我们要去了解JS的运行机制,首先我们得现有一些前置知识,那就是进程和线程的概念。一下摘自百度百科
- 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
对于上面的概念,确实有点不好理解,通过网上查阅对于进程和线程一些比喻和描述,我觉得应该这样去理解它们。
- 从包含关系来看是进程包含线程。
- 我们把CPU看做是一个兵工厂,在这个兵工厂里面有许多个车间,每个车间的任务是不同的,有的造大炮,有的造坦克,还有的造飞机......那么我们可以把每个车间看作是一个进程,假设我们的电脑是单核CPU的,那么相对于我们的这个工厂电力有限,只能够使得一个车间进行运作,其他的车间只有等待当前车间任务完成,腾出电力才能运作。总结出来就是每一个CPU上同一时间只能运行一个进程。
- 对于我们正常运行的进程,也就是我们比喻中的一个车间,在这个车间里面,有许许多多的工人,每一个工人负责具体任务也不一样,有的工人负责炼钢,有的工人负责铸钢,有的工人负责配置弹药......车间中的工人就相当于进程中的线程,但是由于工人们都在一个车间里面,一起使用这个车间里面的工具进行生产,对于我们的线程来说,所有线程都共享同一个进程的资源,如内存等。
JS运行机制
那么从上面我们大致了解了一下什么是进程和线程,现在来讨论JS,首先我们应该都知道JS是一门单线程的解释性语言,单线程就是指,JS的任务始终运行在一个线程里面,虽然有woker一类实现了所谓的“多线程”但是在底层依旧是在用单线程进行实现的。至于什么是解释性语言,JS不像C++、JAVA一样是编译成计算机可执行语言之后再执行的,它是动态编译的,也就是边编译边执行的。那么编译这项工作是由谁来完成呢:浏览器内核或者V8引擎(chrome和NodeJS环境下)。在编程成计算机可执行的语句后,要实现的具体功能,例如操作DOM对象、获取计算机操作系统信息这些功能又是由谁来提供的呢,答案是环境。下面这张图就描述了JS到具体功能实现的大体过程。
通过这张图我们大致了解了JS的运行机制,现在我们来详细描述一下JS引擎和运行环境。
JS引擎
首先关于JS的引擎,大家应该都知道现在最著名的V8引擎了吧,(tips: 浏览器内核是由渲染引擎和JS引擎构成,想要了解细节的同学可以去查阅相关资料),V8引擎是现在chrome浏览器的JS引擎也是NodeJs的引擎。那么这个引擎究竟是何方神圣呢?其实就是一个堆和一个栈。说详细一点就是一个内存堆和一个调用栈,关于堆栈的一些描述大家可以先去看看我之前JavaScript异步机制 末尾对于堆栈的描述。那么下图就是JS引擎的一个模型图
在JS的调用栈中,说白了就是一个个函数的执行,这些函数包括全局的函数、通常意义上的函数和eval函数,在得到JS代码之后,首先会向栈底放入全局代码,然后遇到代码中的函数就会入栈这个函数并执行,基本类型的变量是直接存在栈里面的,引用类型的实际值是放在内存堆中,他在堆中的地址是存放在栈内的。在调用栈中就是实际代码逻辑执行的地方,代码会被划分成一个一个的可执行单元,依次入栈并执行。这些可执行单元就是“执行上下文”,关于具体的执行上下文我会在之后单独写一篇文章来详细描述。回到主题,那么执行栈是怎么执行代码呢?首先将全局代码入栈,在执行它的时候,遇到了其他函数,就将函数入栈并执行,如果发现函数调用了其他的函数,再入栈执行,没有调用了,就在执行完后开始退栈。以下面的代码为例子
function a () {
console.log('this is print')
}
function b () {
a()
}
b() // this is print
// ...其他代码
在调用栈中的执行过程如下
- 首先是全局代码当做一个函数入栈,我先称其为global函数,此时调用栈栈底有一个函数global()
- 在global()中发现有调用b(),此时将b()入栈
- 在b()中发现有调用a(),此时将a()入栈
- 在a()中发现调用了console.log()函数,将其入栈
- 在a()中没有其他函数了,这个时候开始执行console.log()函数,输出完毕后,将其退栈
- 在b()中没有其他函数,将a()退栈
- 在global()中b()已经执行完毕,将其退栈
- 此时例子中的代码执行完毕。
运行过程如下图所示:
在执行栈中处理的代码都是同步的,异步的代码呢,比如事件系统、网络IO、定时器等功能是谁提供的呢,答案是环境,之前也有说到,JS运行时环境提供给了它不同的能力,浏览器环境提供了操作DOM能力,DOM事件系统等;Node环境提供了文件系统,IO系统等能力,这些都是由JS运行时环境所暴露得API来提供的能力。这些API对一些异步任务进行处理,处理完毕后,将结果(成功或者失败)返回给任务队列,等待Event Loop调用如下图所示:
现在来详细解释一下上面这一张比较宏观的JS运行机制.
运行时环境
从上面的这张图我们能够比较宏观的大致了解JS的运行机制,在JS引擎将代码解释并执行之后,当这段代码要实现某个功能的时候,例如操作DOM、监听用户事件等行为的时候,就需要运行环境通过暴露一些API来执行对应的功能,现在JS常见的运行环境有两个,一个是浏览器环境,一个是Node环境,这两个环境分别提供给了JS实现不同功能的能力,像Node环境提供给了JS可以实现读写文件系统、获得操作系统信息等能力。简单来说,在不同环境下的JS可以实现不同的能力和功能。
Event Loop
之前我们也有说到过JS是单线程的语言,并且它是异步的,控制JS异步的实现就要依赖大名鼎鼎的Event Loop了,怎么理解Event Loop呢,因为JS是单线程的原因,每次JS只能够去处理一个任务,其他的任务只能去排队等待,如果是同步任务的话比较好理解,按照代码书写的先后顺序执行即可,但是当遇到了异步任务,真实的任务执行顺序就与书写顺序没有直接关系了,在之前的文章里面大致描述了一下JS异步任务执行机制,现在我们从Event Loop的角度来,不同的异步具体是怎么调度的。首先,从宏任务和微任务的角度,JS的任务队列被分为了:
- 微任务队列
- 宏任务队列
但是这种划分对于了解他的具体调度机制是不够的,我再对其进行划分
-
微任务队列
NextTickTask Queue
用于存放process.nextTick()回调的队列MircoTask Queue
用于存放promise.then/.catch/.finally
回调的队列
-
宏任务队列
-
全局JS代码
-
Timer Queue
用于存放setTimeout
、setInterval
回调的队列 该阶段用于检查,这两个定时器是否到了定时器规定的时间,如果时间到了则将其送入调用栈中执行,时间未到则移动到下一个循环 -
Pengding I/O callbaks Queue
存放用于检查和执行pending状态下的I/O操作回调的队列这个队列是将在pending状态的I/O(例如网络I/O和文件I/O等)的回调放入队列中,当这个I/O完成的时候,将回调送往调用栈中,这个结果可能是成功也有可能是失败
-
Idle/prepare Queue
libuv库内部机制,我们不用关心 -
Poll Queue
轮询I/O状态的队列这是Event Loop中最重要的阶段,这个阶段是用于轮询是否有新的I/O,轮询的对象也是I/O设备,如果有那么则在
Pengding I/O callbaks Queue
中添加新的任务 -
Check Queue
用于执行setImmediate
回调的队列检查
setImmediate
是否到达触发时机,机制同Timer
阶段 -
Close Queue
用于关闭网络连接的队列执行关闭事件的回调,比如socket的
close
的回到
-
以上是任务队列的具体的执行机制,在宏任务队列里面只要还有队列没有被清空Event Loop也就会一直循环,直到所有任务都被清空,然后在每一次循环完Event Loop之后,环境都回去检测微任务队列是否为空,如果为空则开始进行Event Loop,但是一旦不为空,系统会优先执行微任务队列的任务,我们在之前也抽象出两个微任务队列,这两个队列中NextTickTask Queue
拥有更高的优先级,所以在异步任务里面,porcess.nextTick()的回调是最早执行的,可以把它理解为他会在同步任务执行完之后就执行,也可以理解为他是异步任务里面最早开始执行的。
以上就是JS的运行机制,可能有一部分地方理解不正确或者不够完善,之后我也会慢慢进行补充。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!