进程、线程
- 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
- 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
浏览器内核
- 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
- 浏览器内核有多种线程在工作。
-
GUI 渲染线程:
- 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
- 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
-
JS 引擎线程:
- 单线程工作,负责解析运行 JavaScript 脚本。
- 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
-
事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
-
定时器触发线程:
- 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
- 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
-
http 请求线程:
- http 请求的时候会开启一条请求线程。
- 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
-
javascript 堆(heap)、栈(stack)、队列(queues)
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
-
heap是没有结构的,数据可以任意存放。heap用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象
-
stack是有结构的,每个区块按照一定次序存放(后进先出),stack中主要存放一些基本类型的变量和对象的引用,存在栈中的数据大小与生存期必须是确定的。可以明确知道每个区块的大小,因此,stack的寻址速度要快于heap。
-
queues(消息队列):一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。
浏览器中的Event Loop
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
task (MacroTask、宏任务)
- setTimeout
- setInterval
- I/O
- script代码块
- setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)
- UI Rendering
jobs (MicroTask、微任务)
- nextTick
- callback
- Promise
- process.nextTick
- Object.observe(废弃)
- MutationObserver
Event loop
==事件循环的顺序,决定了JavaScript代码的执行顺序。== 它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的宏任务,这样一直循环下去。其中每一个任务的执行,无论是微任务还是宏任务,都是借助函数调用栈来完成。
一段代码块就是一个宏任务。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
不同任务类型会进入对应的event queue,比如setTimeout和setInterval会进入相同的宏任务Event queue
浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。
具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。
setTimeout(function() { console.log('setTimeout');})new Promise(function(resolve) { console.log('promise'); resolve ('true')}).then(function() { console.log('then');})console.log('console');//这段代码作为宏任务,进入主线程。 //先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。//接下来遇到了Promise,new Promise立即执行,遇到console.log输出promise,then函数分发到微任务Event Queue。//遇到console.log(),立即执行输出console。 //好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,then。 //ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。//我们发现了宏任务 Event Queue中setTimeout对应的回调函数,立即执行输出setTimeout。 //结束,完整的输出为 promise => console => then => setTimeout复制代码
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end');}async function async2() { console.log('async2');}console.log('script start');setTimeout(function() { console.log('setTimeout1');}, 200);setTimeout(function() { console.log('setTimeout2'); new Promise(function(resolve) { resolve(); }).then(function() { console.log('then1') }) new Promise(function(resolve) { console.log('Promise1'); resolve(); }).then(function() { console.log('then2') })},0)async1();new Promise(function(resolve) { console.log('promise2'); resolve(); }).then(function() { console.log('then3'); }); console.log('script end');//整体script作为第一个宏任务进入主线程,async1(),和async12()函数申明,但并没有执行,//遇到console.log输出script start。//继续向下执行,遇到setTimeout,把它的回调函数放入宏任务Event Queue。(ps:暂且叫他setTimeout1)//继续向下执行,又遇到一个setTimeout,继续将他放入宏任务Event Queue。(ps:暂且叫他setTimeout2)//遇到执行async1(), 进入async的执行上下文之后,遇到console.log输出 async1 start//然后遇到await async2(),由于()的优先级高,所有立即执行async2(),进入async2()的执行上下文。//看到console.log输出async2,之后没有返回值,结束函数,返回undefined,返回async1的执行上下文的await//undefined,由于async函数使用await后得语句会被放入一个回调函数中,所以把下面的放入微任务Event Queue中。//结束async1() 遇到Promise,new Promise直接执行,输出Promise2。then后面的函数被分发到微任务Event Queue中//执行完Promise(),遇到console.log,输出script end,这里一个宏任务代码块执行完毕。//在主线程执行的过程中,事件触发线程一直在监听着异步事件,//当主线程空闲下来后,若微任务队列中有任务未执行,执行的事件队列(Event Queue)中有微任务,//遇到new Promise()后面的回调函数,执行代码,输出then3。//看到 async1中await后面的回调函数,执行代码,输出async1 end//(注意:如果俩个微任务的优先级相同那么任务队列自上而下执行,但是promise的优先级高于async,所以先执行promise后面的回调函数)//那么第二轮时间循环从setTimeout宏任务开始://setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event//Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event//Loop时重新判断。因为setTimeout1有200ms的延时,并没到达指定时间,所以先执行setTimeout2这个宏任务//进入到setTimeout2,遇到console.log首先输出setTimeout2;//遇到Promise,new Promise直接执行。then后面的函数被分发到微任务Event Queue中//再次遇到Promise,new Promise直接执行输出promise1。then后面的函数被分发到微任务Event Queue中//主线程执行执行空闲,开始执行微任务队列中依次输出then1和then2。//第二轮事件循环正式结束。第二轮依次输出promise1 => then1 => then2//现在任务队列中只有个延时200ms的setTimeout1,在到达200ms后执行setTimeout的回调函数输出setTimeout1//时间循环结束//结束,完整的输出为script start => async1 start => async2 => promise2 => script end => then3 =>async1 end //=>setTimeout2 => Promise1 => then1 => then2 => setTimeout1复制代码
总结
首先要明白的是event loop是由javascript宿主环境(像浏览器)来实现的,js引擎他不关心也不知道event loop机制的运行和存在,他只负责从事件队列里面读取事件来执行,他不会也不会知道怎样向事件队列中push事件任务,这些都由宿主来完成。理解这第一点很重要要先知道是谁在做这件事情。
第二点就是一个宿主环境 只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。相同任务源的任务,只能放到一个任务队列中。不同任务源的任务,可以放到不同任务队列中。然后js引擎做的事就是不断的去读取这些队列里面的任务来执行,
任务可分为宏任务和微任务;他们的执行过程和顺序:js引擎逐句的执行script整体代码,当遇到异步任务时,js的运行环境就会在适时的时候将这些事件任务push到相应的队列中去,等待着被js引擎去执行,而如果异步没有产生回调(callback)或者说是事件任务,那他就不会push到队列里面去,当js执行栈执行完成后,然后他把微任务队列中的任务读取过,并进行执行,在这执行过程中如果有产生新的异步任务也会按照上述的方式进行处理,当微任务执行完成后他会去读取宏任务队列中的任务并执行,然后周而复始的反复执行,直到把队列中的任务全部执行完。
macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;
micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver至于队列中的优先级,有一个大概的顺序process.nextTick > promise.then > setTimeout > setImmediate;优先级执行顺序可能还会和具体的宿主环境有关;边城大神也说的对对于异步我们更不应该去依赖他们的执行顺序既然是异步就当作无序的来处理。