单线程的JavaScript
JavaScript 是单线程的,主要是因为 JavaScript 的主要用途是与用户交互,以及操作 DOM。如果 JavaScript 是多线程,那么当多个线程同时操作一个 DOM 元素时,那么就无法保证哪个线程优先去操作该 DOM 元素。
也许,有小伙伴会说:“我们可以通过HTML 5的 web worker 来创建 JavaScript 的多个线程啊”。是的,web worker 允许我们创建多个线程来执行 JavaScript,但创建的都是子线程,这些子线程都受到 JavaScript 唯一的主线程控制,而且这些子线程是不能够操作 DOM 元素的.
由于 JavaScript 的单线程机制,JavaScript代码是按照任务执行的,而任务分为同步任务和异步任务
同步任务:在主线程中执行,一个接着一个按顺序执行
异步任务:一开始就进入到一个任务队列中,主线程执行完当前任务后,就会去读取任务队列,某个异步任务可以开始了,该任务则会进入主线程执行
所以,JavaScript 执行的顺序是,先执行主线程上已有的任务,然后再读取“任务队列”,将“任务队列”中的任务加到主线程中继续执行。
任务队列,是一个事件队列,如果事件指定过回调函数,这些事件发生时就进入“任务队列”,等待主线程读取。我们常说的回调函数,也就是被主线程暂时挂起不执行的代码,当事件发生时,主线程才会将这代码加入进来执行。注意,同步任务总是在主线程读取“任务队列”之前执行的
事件循环(Event Loop)
主线程读取“任务列表”这一过程是循环不断的,这种运行机制称为事件循环。
定时器
任务队列,不仅仅存放了异步任务的事件,还存放了定时事件(setTimeout,setInterval)
定时器 setTimeout 和 setInterval 只是把事件放置到任务队列中,必须等待任务队列中的事件执行完毕,主线程才会调用定时器相应的回调函数。
Microtasks 和 Macrotasks
任务队列不止一个,还包括了 microtasks(微任务) 和 macrotasks(宏任务)
microtasks
- process.nextTick
- promise
- Object.observe
- MutationObserver
macrotasks
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
- 一个事件循环中会有一个或多个任务队列
- 任务队列就是宏任务(macrotasks)
- 每个事件循环都有一个 microtasks queue
- task queue == macrotasks queue != microtasks queue
主线程拥有一个执行栈和一个事件循环
因此,JavaScript 运行机制是这样的:
首先,全部代码的 script 是一个 macrotask。
第一次事件循环,主线程先执行同步任务(其实也是macrotask),执行过程中会创造新的macrotask,接着继续执行,然后会把一些事件,如promise(发现 then 方法,则放入 microtask),放置在 microtask 中。如果有 microtask 则执行 microtask,没有则进入下一次循环。
总结为:同步完成,一个宏任务,所有微任务,一个宏任务,所有微任务…
最后,看以下这道面试题
|
|
输出:
|
|