前言
本文主要是关于JS 事件循环的简单理解
一、线程和进程
首先我们需要了解什么是进程和线程
1.1 进程
计算机中的程序关于某数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 相当于运行中的程序的描述,一个运行的程序就对应着一个进程。 单核 CPU 再同一时间内有且只有一个进程再运行。
1.2 线程
是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的一个执行路径,一个进程可以包含多个线程。线程可以共享所在进程的地址空间和其他资源。
二、JS 是单线程
2.1 原因
JS 负责处理页面中用户的交互,以及操作 DOM 树、CSS 样式树。这决定了它只能是单线程,如果 JS 是多线程来操作 DOM,则可能出现冲突。 比如 JS 两个线程同时操作一个 DOM,一个线程需要浏删除DOM节点,而另一个线程却需要修改这个节点,这个时候浏览器就无法决定采用哪一种策略了 。
而单线程就意味着所有任务要按顺序完成。这样 JS 就不会显得慢吗?为了避免 IO 设备(比如:上传,读取文件,下载等)过慢,JS 不管 IO 设备,先运行排在后面的任务,等 IO 设备返回了结果,再执行下去。这样,就有了两个任务,一个是同步任务,另一个是异步任务。
三、同步任务与异步任务
3.1 同步任务
指在主线程排队执行的任务,前一个任务执行完毕后,才能执行后一个任务,执行的顺序和任务的排列顺序是一致的。
示例:
console.log('begin') ;
function bar () {
console.log('1') ;
}
function foo () {
console.log('2') ;
bar();
}
foo() ;
console.log('end');
输出的顺序为: begin 2 1 end
3.2 异步任务
异步任务会被主线程挂起,不会进入主线程,而是进入任务队列,只有队列通知主线程某个异步任务可以执行,并且执行栈为空时,对应的任务才会进入执行栈获得执行的机会。
示例:
console.log('begin') ;
setTimeout(function timer1 () {
console.log('1')
}, 1800)
setTimeout(function timer2 () {
console.log('2')
setTimeout(function inner () {
console.log('3')
}, 1000)
}, 1000)
console.log('end');
输出的顺序为:begin end 2 1 3
3.3 异步任务的运行方式
主线程会先运行所有的同步任务,在碰到需要异步执行的任务时,会将异步任务挂起,异步任务准备完毕后会进入浏览器的任务队列里。任务队列是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。当同步任务处理完,主线程就根据服务器的处理先后顺序执行剩余的异步任务。
以上面示例为准,在运行到 setTimeout 这类定时任务时,会将它们放在一边,主线程继续向下执行,所以第二个为 end 。当定时时刻达到时,它们按先后顺序放入任务队列中等待主线程执行。当主线程空闲时,它们按先入先出的顺序来执行。所以分别输出 2 1 3。
如下图:
3.4 js 中常见的异步执行代码
1.ajax请求
2.定时器
3.事件处理函数:
(1)延迟类:setTimeout、setInterval、requestAnimationFrame、setImmediate。
(2)监听事件实现的类型:监听 new Image 加载状态、监听 script 加载状态、监听 iframe 加载状态、Message。
(3)带有异步功能类型 Promise、ajax等等。
四、事件循环
我们可以发现,上图中事件队列与最后的执行是双向的,表示当主线程内的任务执行完毕时,会循环不断地从任务队列中读取事件,在主线程中执行,这里就是我们常说的事件循环了。
五、宏任务和微任务
在异步任务被挂起时,会分为两种任务队列,分别为宏任务队列和微任务队列。
5.1 常见的宏任务
- setTimeout
- setInterval
- I/O
- script 代码…
5.2 常见的微任务
- callback
- Object.observe
- Promise的回调
- process.nextTick …
5.3 执行顺序
首先执行主线程中的宏任务,碰到宏任务放入宏任务队列,碰到微任务放入微任务队列。主线程执行完毕后,执行任务队列里的微任务,然后执行下一个宏任务,顺序如上。