我们首先要知道JavaScript是一门单线程的语言,顾名思义"单线程”,就是指一次只能执行一个任务,如果有多个任务,那就必须排队执行,在上一个任务执行完毕之后,再去执行后面的任务,以此类推。如果一个任务耗时过长,那么后面的任务就必须等待这个耗时过长的任务完成,才能继续往下执行,那么这种情况会造成什么后果呢?拖延我们的程序执行,常见的浏览器无反应。于是,JavaScript将所有任务分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
一、JS运行机制
Js是单线程语言,它是基于事件循环,事件循环大致分为以下几个步骤:
1.所有同步任务都在主线程上执⾏, 形成⼀个执⾏栈(execution context stack)。
2.主线程之外, 还存在⼀个"任务队列"(task queue) 。只要异步任务有了运⾏结果, 就在"任务队列"之中放置⼀个事件。
3.一旦"执⾏栈"中的所有同步任务执⾏完毕, 系统就会读取"任务队列", 看看⾥⾯有哪些事件。那些对应的异步任务, 于是结束等待状态, 进⼊执⾏栈, 开始执⾏。
4.不断重复第三步,直至"异步任务队列"全部清除。
同步任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。
异步模式的四种方式:
1.回调函数callback
2.事件驱动 Event-Driven
3.观察者模式Observer pattern(又称发布订阅模式publish-subscribe pattern)
4.promise模式
5.宏任务(计时器, ajax, 读取文件)
6.setImmediate
讲了JavaScript的运行机制,那么浏览器是怎么去执行JavaScript的代码的呢?
二、浏览器的事件循环
JavaScript是如何执行的
JavaScript 并不是一行一行的分析并执行代码的,而是分段一段一段的进行分析并执行。
这里要说明一下,怎么才算一段。这里的段指的是 JavaScript 中的执行上下文。
执行上下文
全局执行上下文:就是指全局代码
函数执行上下文:这里指的是一个一个函数,一个函数执行前会先创建一个执行上下文
eval执行上下文:eval不推荐使用
由此可见当JavaScript运行时会有很多执行上下文,为了方便管理,js引擎会创建一个执行上下文栈来对这些执行上下文进行管理,遵循先进后出原则。
全局上下文在栈的最底部,全局上下文只有一个在浏览器关闭时出栈。
函数在执行时会创建函数执行上下文,并放入执行栈中执行,执行完毕后出栈。
Event loop 循环机制
当 JavaScript 执行时,会将全局执行上下文放入执行栈中,接下来遇到函数执行上下文时会将这个上下文也放入执行栈中,执行完毕会出栈,当执行栈为空时,会从任务队列头部拿取一个任务,创建上下文并放入执行栈中执行。每当执行栈为空时总会循环的从任务队列获取任务,并创建执行上下文放入执行栈执行。这个循环我们称之为事件循环。
如果在Node环境中去执行JavaScript脚本语言,Node.js怎么去处理JavaScript异步任务的呢?
三、Node的事件循环
当 Node.js 启动时,它会初始化事件循环,处理提供的脚本,同步代码入栈直接执行,异步任务(网络请求、文件操作、定时器等)在调用 API 传递回调函数后会把操作转移到后台由系统内核处理。目前大多数内核都是多线程的,当其中一个操作完成时,内核通知 Node.js 将回调函数添加到轮询队列中等待时机执行。
Node.js事件循环的六个阶段
1.timers(定时器阶段)
首先事件循环进入定时器阶段,用于存储定时器的回调函数(setTimeout setInterval)。
2.pending callbacks
执行与操作形同相关的回调函数。比如启动服务器应用时监听端口操作时的回调函数。
3.idle,prepare
idle, prepare 阶段是给系统内部使用,idle 这个名字很迷惑,尽管叫空闲,但是在每次的事件循环中都会被调用,当它们处于活动状态时。
4.poll
存储I/O操作相关的回调函数,比如文件读写操作的回调函数。如果时间队列中有回调函数,执行他们直到清空操作。否则事件循环在此阶段会停留一段时间以等待新的回调含税进入。这个等待取决于两个条件:
(1).setImmediate队列(check阶段)存在要执行的回调函数。
(2).timers队列中存在要执行的回调函数,在这种情况下,事件循环将移至check阶段,然后移至Closing callbacks阶段,并最终从timers阶段进入下一次循环。
5.check
存储setImmediate的回调函数。
6.Closing callback
执行与关闭事件相关的回调函数。比如:关闭数据库连接的回调函数。
四、宏任务与微任务
上面讲了浏览器和Node.js是如何处理同步和异步任务的,那么异步任务就只有一种吗?
任务队列的分类
任务队列分为两种,一种叫宏任务(macrotask),一种叫微任务(microtask )。
宏任务(macrotask)
script( 整体代码)、setTimeout、setInterval、I/O(http请求)、UI 渲染。
微任务(microtask)
Promise.then()、MutationObserver(监听dom的更改)。
两者的区别
执行原则
执行完一个宏任务回去检测微任务队列是否为空,如果不为空则执行完队列内的所有微任务,如果队列为空,则继续执行下一个宏任务,下一个宏任务执行完后会继续检测微任务队列是否为空,往复循环。
五、JS任务队列
我们上面讲了异步任务分为宏任务和微任务都是在任务队列里面去等待执行,那么任务队列是不是也分为宏队列和微队列呢?
说起任务队列的话,首先我们要回顾一下JS语言的特点。
我们知道,Javascript 这门脚本语言诞生的使命就是为处理页面中用户的交互,以及操作 DOM 而诞生的。
所以JS的设计就是单线程的,总不能多线程来操作DOM结构吧(那不就乱套了吗)。
那么什么是单线程,其实就是任务一个接着一个做,不能同时处理多个任务。
那这样就会导致一个问题,如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
那JS是如何解决这个问题呢?我用一张图来说一下:
接下来我们来看一段代码
console.log(1)
setTimeout(function () {
console.log(2);
process.nextTick(function () {
console.log(3);
})
})
Promise.resolve().then(function() {
console.log(4)
}).then(function() {
console.log(5)
})
我们来看一下这个代码的执行过程。
反正肯定不是输出12345。
从上到下:
第一遍
console.log(1)会放到主线程中,
setTimeout会放到宏任务队列
Promise会放到微任务队列
主线程先执行,然后微任务:
打印1,4,5
再执行宏任务:
console.log(2)放到主线程
process.nextTick放到微任务队列
主线程先执行,然后微任务
打印2,3
所以打印结果为:14523
OK,那么关于Js的任务队列也就解释清楚了
总结
同步(Sync)发出一个功能调用时,必须一件一件事做,等前一件做完了才能做下一件事。
异步(Async)当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。
总结来说,同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。