单线程
首先我们都知道,JavaScript是一门单线程的语言,所谓单线程指的是在JavaScript引擎中负责解释和执行代码的线程只有一个,通常称为主线程。那么为什么JavaScript必须是单线程的语言,而不能像他的老大哥Java一样,手动开启多个线程呢?
因为这是由于JavaScript所运行的浏览器环境决定,他只能是单线程的。试想一下,如果JavaScript能开启多个线程,页面上有一个div,我们同时在多个线程中来改变这个div中的内容,那么最终这个div会变成什么样子谁也确定不了,最后只能听天由命,看哪个线程是最后一个运行结束的。
因此多线程带来了很多的不确定性,为了避免这种问题,JavaScript必须是单线程。
虽然JavaScript是单线程运行的,但是还是存在其他线程的;例如:处理Ajax请求的线程、定时器的线程、读写文件的线程(nodejs中)等。
同步任务和异步任务
因为JavaScript是单线程运行的,所有的任务只能在主线程上排队执行;但是如果某个任务特别耗时,比如Ajax请求一个接口,可能1s返回结果,也可能10s才返回,有很多的不确定因素(网络延迟等);如果这些任务也放到主线程中去,那么会阻塞浏览器(用户除了等,不能进行其他操作)。
于是,浏览器就把这些任务分派到异步任务队列中去,并且跟他们说:你们自己去后台玩儿,等你们好了再过来通知我!
事件循环
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程发现有异步任务,就在“任务队列”之中加入一个任务事件。
- 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”(先进先出原则)。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步,这样的一个循环称为事件循环。
宏任务与微任务
如果任务队列中有多个异步任务,那么先执行哪个任务呢?于是在异步任务中,也进行了等级划分,分为宏任务(macrotask)和微任务(microtask);不同的API注册的任务会依次进入自身对应的队列中,然后等待事件循环将它们依次压入执行栈中执行。
宏任务
- script(整体代码)
- setTimeout, setInterval, setImmediate,
- I/O
- UI rendering
微任务
- process.nextTick
- Promise
- Object.observe(已废弃)
- MutationObserver(html5新特性)
我们可以把整体的JS代码也看成是一个宏任务,主线程也是从宏任务开始的。我们把上面事件循环的步骤更新一下:
1.执行一个宏任务
2.执行过程中如果遇到微任务就加入微任务队列,遇到宏任务就加入宏任务队列
3.宏任务执行完毕后,检查当前微任务队列,如果有,就依次执行(一轮事件循环结束)
4.开始下一个宏任务
思路:
1.第一轮循环开始
2.打印script start
3.发现setTimeout,放入宏任务1
4.打印async1 start
5.打印async2
6.把await async2函数后面的回调放入微任务1
7.打印promise1
8.把then中的函数放入微任务2
9.打印script end
10.调用栈清空,开始执行微任务1,打印async1 end
11.执行微任务2,打印promise2
12.微任务执行完,第一轮循环结束
13.开始宏任务1,打印setTimeOut