JS三座大山再学习 ---- 异步和单线程

本文已发布在西瓜君的我的博客,原文传送门javascript

前言

写这一篇的时候,西瓜君查阅了不少资料和文章,可是至关多的文章写的都很简单,甚至互相之间有矛盾,这让我很困扰;同时也让我坚决了要写出一篇好的关于JS异步、单线程、事件循环的文章,下面,让咱们一块儿来学习本文吧,冲鸭~~html

单线程

### 1. 什么是单线程html5

//栗子1
console.log(1)
console.log(2)
console.log(3)
//输出顺序 1 2 3

单线程即同一时间只作一件事java

2. JavaScript为何是单线程

  • 首先是历史缘由,在建立 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并很差。
  • 其次是由于多线程的复杂性,多线程操做须要加锁,编码的复杂性会增高。
  • 并且,若是同时操做 DOM ,在多线程不加锁的状况下,最终会致使 DOM 渲染的结果不可预期

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。node

异步

1.JS的 同步任务/异步任务

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务web

  • 异步:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行chrome

//异步的栗子
console.log(1)

setTimeout(()=>{
    console.log(2)
},100)

console.log(3)
//输出顺序 1 3 2

2. JavaScript为何须要异步

若是在JS代码执行过程当中,某段代码执行太久,后面的代码迟迟不能执行,产生阻塞(即卡死),会影响用户体验。api

JavaScript怎么实现异步

JS 实现异步时经过 事件循环(Event Loop),下面咱们来了解一下浏览器

1.执行栈与任务队列

先理解几个概念markdown

  • JS任务 分为同步任务(synchronous)和异步任务(asynchronous)
  • 同步任务都在 JS引擎线程(主线程) 上执行,造成一个执行栈(call stack)
  • 事件触发线程 管理一个 任务队列(Task Queue)
  • 异步任务 触发条件达成,将 回调事件 放到任务队列(Task Queue)中
  • 执行栈中全部同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行

当一个JS文件第一次执行的时候,js引擎会 解析这段代码,并将其中的同步代码 按照执行顺序加入执行栈中,而后从头开始执行。若是当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,而后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码所有执行完毕。

举个栗子:

//Event loop

//(1)
console.log(1)

//(2)
setTimeout(()=>{
    console.log(2)
},100)

//(3)
console.log(3)
  1. 先解析整段代码,按照顺序加入到执行栈中,从头开始执行
  2. 先执行(1),是同步的,因此直接打印 1
  3. 执行(2),发现是 setTimeout,因而调用浏览器的方法(webApi)执行,在 100ms后将 console.log(2) 加入到任务队列
  4. 执行(3),同步的,直接打印 3
  5. 执行栈已经清空了,如今检查任务队列,(执行太快的话可能此时任务队列仍是空的,没到100ms,尚未将(2)的打印加到任务队列,因而不停的检测,直到队列中有任务),发现有 console.log(2),因而添加到执行栈,执行console.log(2),同步代码,直接打印 2 (若是这里是异步任务,一样会再走一遍循环:-->任务队列->执行栈)

因此结果是 1 3 2

注:setTimeout/Promise等咱们称之为任务源。而进入任务队列的是他们指定的回调

2.宏任务(macro task)与微任务(micro task)

上面的循环只是一个宏观的表述,实际上异步任务之间也是有不一样的,分为 宏任务(macro task) 与 微任务(micro task),最新的标准中,他们被称为 taskjobs

  • 宏任务有哪些:script(总体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering(渲染)
  • 微任务有哪些:process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

下面咱们再详细讲解一下执行过程

执行栈在执行的时候,会把宏任务放在一个宏任务的任务队列,把微任务放在一个微任务的任务队列,在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。若是微任务队列不存在,那么会去宏任务队列中 取出一个任务 加入当前执行栈;若是微任务队列存在,则会依次执行微任务队列中的全部任务,直到微任务队列为空(一样,是吧队列中的事件加到执行栈执行),而后去宏任务队列中取出最前面的一个事件加入当前执行栈...如此反复,进入循环。

注:

  • 宏任务和微任务的任务队列均可以有多个
  • 当前执行栈执行完毕时会马上先处理全部微任务队列中的事件,而后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务以前执行。
  • 不一样的运行环境 循环策略可能有不一样,这里探讨chrome、node环境

举个栗子:

//Event loop


//(1)
setTimeout(()=>{
    console.log(1)
},100)

//(2)
setTimeout(()=>{
    console.log(2)
},100)

//(3)
new Promise(function(resolve,reject){
    //(4)
    console.log(3)
    resolve(4)
}).then(function(val){
    //(5)
    console.log(val);
})

//(6)
new Promise(function(resolve,reject){
    //(7)
    console.log(5)
    resolve(6)
}).then(function(val){
    //(8)
    console.log(val);
})

//(9)
console.log(7)

//(10)
setTimeout(()=>{
    console.log(8)
},50)

*上面的代码在node和chrome环境的正确打印顺序是 3 5 7 4 6 8 1 2

下面分析一下执行过程:

  1. 所有代码在解析后加入执行栈
  2. 执行(1),宏任务,调用webapi setTimeout,这个方法会在100ms后将回调函数放入宏任务的任务队列
  3. 执行(2),同(1),可是会比(1)稍后一点
  4. 执行(3),同步执行new Promise,而后执行(4),直接打印 3 ,而后resolve(4),而后.then(),把(5)放入微任务的任务队列
  5. 执行(6),同上,先打印 5 ,再执行resolve(6),而后.then()里面的内容(8)加入到微任务的任务队列
  6. 执行(9),同步代码,直接打印 7
  7. 执行(10),同(1)和(2),只是时间更短,会在 50ms 后将回调 console.log(8) 加入宏任务的任务队列
  8. 如今执行栈清空了,开始检查微任务队列,发现(5),加入到执行栈执行,是同步代码,直接打印 4
  9. 任务队列又执行完了,又检查微任务队列,发现(8),打印 6
  10. 任务队列又执行完了,检查微任务队列,没有任务,再检查宏任务队列,此时若是超过了50ms的话,会发现 console.log(8) 在宏任务队列中,因而执行 打印 8
  11. 依次打印 1 2

注:由于渲染也是宏任务,须要在一次执行栈执行完后才会执行渲染,因此若是执行栈中同时有几个同步的改变同一个样式的代码,在渲染时只会渲染最后一个

结语

写到这里,仍然以为还有不少知识点没有写出来,可是想写又不知道从哪里入手。因而决定今天就写到这里,往后再作补充。
到这篇,JS三座大山系列就暂时完结了,在这其中本身也学到了不少,但愿能继续输出一些有意义的东西,加油,西瓜君~~

参考文章:

https://www.jianshu.com/p/12b9f73c5a4f
http://www.javashuo.com/article/p-fliavhij-eb.html

若有错误,请斧正

以上

 

出处:http://www.javashuo.com/article/p-dztrnnnq-bx.html