1。单线程JavaScriptJavaScript是一种单线程语言,这是由它的用途决定的。作为一种浏览器脚本语言,它主要负责与用户交互和操作DOM。如果JavaScript是多线程的,有两个线程同时操作一个DOM节点,一个负责删除DOM节点,一个负责向DOM节点添加内容,浏览器应该使用哪个线程作为标准?所以,JavaScript的目的决定了它只能是单线程的,过去是,以后也不会变。HTML5WebWorker允许JavaScript主线程创建多个子线程,但是这些子线程完全由主线程控制,不能操作DOM节点,所以JavaScript单线程的本质没有改变。2.同步任务和异步任务JavaScript是一种单线程语言,这意味着任务需要排队等待执行。只有完成了前一个,才能执行后一个。如果之前的任务很耗时怎么办?比如操作IO设备、网络请求等,后续任务会被阻塞,页面会卡住,甚至崩溃,用户体验很差。如果JavaScript的主线程遇到这些耗时任务,就将其挂起,先执行后面的任务,等待挂起的任务有结果再返回执行,可以解决耗时任务阻塞主线程的问题.因此,所有的任务都可以分为两种,同步任务和异步任务。同步任务在主线程中执行,异步任务挂起不进入主线程执行(让主线程阻塞等待)。当有结果时,再放入主线程中执行。3.任务队列和事件循环3.1任务队列是事件队列,也可以理解为消息队列。当挂起的异步任务就绪时,它会在任务队列中放置一个相应的事件,表示该任务可以进入线程中执行的main。任务队列中的事件,除了IO设备事件外,还包括网络请求、鼠标点击、滚动等,只要为事件指定回调函数,这些事件就会在发生时进入任务队列,等待供主线程读取,然后执行相应的回调函数。回调函数其实就是一个挂起的异步任务,比如:Ajax请求,请求成功或者失败后执行的回调函数就是一个异步任务。任务队列是先进先出的数据结构。只要主线程为空,就会首先读取排在最前面的事件。3.2事件循环主线程从任务队列中读取事件。这个过程是连续不断的,所以JavaScript的运行机制也被称为EventLoop(事件循环)。4、宏任务和微任务异步任务又可以分为宏任务和微任务,对应的任务队列有两个,分别是宏任务队列和微任务队列。4.1宏任务setTimeout,setInterval,setImmediate会生成宏任务4.2微任务requestAnimationFrame,IO,读取数据,交互事件,UIrender,Promise.then,MutationObserve,process.nextTick会生成微任务4.3JavaScript脚本在浏览器进程中执行4.3.1过程描述JavaScript脚本进入主线程,开始执行b。如果在执行过程中遇到宏任务和微任务,则分别挂起,只有当任务就绪时,事件才放入对应的任务队列c中。脚本执行完毕,执行栈清空。d.去microtask队列中逐一读取事件,将相应的回调函数放入执行栈中运行。如果在执行过程中遇到macrotasks和microtasks,处理方法同b。任务队列为空e.浏览器执行渲染动作,GUI渲染线程接管,直到渲染完成。F。JS线程接管,从宏任务队列中依次读取事件,将对应的回调函数放入执行栈,开始下一个宏任务的执行,流程为b->c->d->e->f,所以循环g。直到执行栈、宏任务队列、微任务队列都为空,脚本执行结束。4.3.2示例4.3.2.1示例1//脚本console.log(1)setTimeout(()=>{console.log(2)},0)constp=newPromise((resolve)=>{setTimeout(()=>{console.log(3)resolve()},1000)console.log(4)})p.then(()=>{console.log(5)})console.log(6)执行过程A。将脚本放入执行栈,开始Executeb。执行到console.log(1),输入1c。执行到setTimeout,遇到一个宏任务,挂起它,因为延时是0ms,4ms后会在宏任务队列中产生一个定时事件,我们称之为定时Ad。程序继续向下执行,执行newPromise(),并运行其参数,遇到第二个定时任务(宏任务),称其为定时B,将其挂起,执行console.log(4),输出4e。遇到微任务p.then(),挂起它f.向下执行时,遇到console.log(6),输出6g。清空执行栈,读取microtask队列,发现是空的,因为p.then()还没有准备好,它的准备好依赖于第一个定时任务(TimedA)的执行h。执行栈为空,微任务队列为空,执行浏览器的渲染动作。我。读取macrotask队列,读取第一个就绪的macrotask,设置为定时任务A,回调函数放入执行栈开始执行,执行console.log(2),输入2j。执行栈清空,microtask队列为空,renderingk开始执行下一个就绪的宏任务,定时任务B,回调将函数放入执行栈执行,执行console.log(3),输出3,并执行resolve(),p.then()就绪,将对应的事件放入微任务队列o。清除执行栈,读取microtaskQueue,如果不为空,读取第一个就绪事件,将其对应的回调函数放入执行栈执行,执行console.log(5),输出5p。执行栈清空,微任务队列为空。Render,然后发现宏任务队列为空,脚本执行完毕。输出结果为:1462354.3.2.2例2asyncfunctionasync1(){console.log('async1_1')awaitasync2()console.log('async1_2')}asyncfunctionasync2(){console.log('async2')}console.log('scriptstart')setTimeout(()=>{console.log('setTimeout')},0)async1()newPromise(resolve=>{console.log('promiseexecutor')resolve()}).then(()=>{console.log('promisethen')})console.log('脚本结束')表示在函数实际返回一个promise之前添加async,比如这里的async2函数,返回一个立即解析的promiseawait会完成后续同步代码(async2)的执行,然后放弃线程转异步任务(Promise.then)挂起,立即解决的promise,所以会在微任务队列中加入一个事件,结果会在后面的Promise.then之前输出。如果理解了前面的例子,再饿着肚子解释一下例子,答案就出来了:scriptstart=>async1_1=>async2=>promiseexecutor=>scriptend=>async1_2=>promisethen=>setTimeout过程是像这样:先执行一个macrotask,然后执行所有的microtask,再执行一个macrotask,再执行所有的microtask...如此反复,执行栈和任务队列都是空的JavaScript脚本在4.4node.jsExecutionprocessJavaScript脚本执行过程在node.js和浏览器中有些不同。造成这些差异的原因是浏览器中只有一个宏任务队列,而node.js中有多个宏任务队列,而这些宏任务队列之间也有执行顺序,微任务穿插在其中这些宏任务。4.4.1各事件类型的执行顺序,执行顺序为从上到下┌────────────────────────────────────────────┐┌──>│定时器│<——————执行setTimeout()和setInterval()的回调│└────────────┬────────────────┘||<--先执行process.nextTick,再执行MicroTaskQueue的回调│┌──────────────┴────────────────在││PENDINGCALLBACKS│<——————执行I/OReturn的I/OReturn│└────────────────────────┘||<--先执行process.nextTick,再执行MicroTaskQueue的回调│┌────────────────┴──────────────────────────────────────┐││idle,prepare│<————内部调用(可以忽略)│└────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘|────────────────┐│┌──────────┴──────────────────────────┐│传入:│-(执行几乎所有回调,除了由|||||安排的关闭回调和由||轮询|<-----|连接安排的计时器和setImmediate(),|会在合适的时候在这个阶段阻塞)│││|││└────────────┬──────────────┘│数据等││|||||└──────────────────┘||<--先执行process.nextTick,再执行MicroTaskQueue的回调|┌──────────┴──────────────┐││检查│<——————setImmediate()回调将在这个阶段执行│└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘|<————socket.on('close',...)└──────────────────────────────────────────────────┘4.4.2示例4.4.2.1基本示例console.log(1)setTimeout(()=>{console.log('timer1')Promise.resolve().then(()=>{console.log('promise1')})},0)setTimeout(()=>{console.log('timer2')Promise.resolve().then(()=>{console.log('promise2')})},0)控制台.log(2)这段代码在浏览器中的执行结果是:12timer1promise1timer2promise2在node.js中的执行结果是:12timer1timer2promise1promise24.4.2.2setTimeout和setImmediate的顺序两者从上图的顺序很明显,timers队列是在check队列中执行的,但是有一个前提,就是事件已经Ready了setTimeout(()=>{console.log('timeout')},0)setImmediate(()=>{console.log('immediate')})上述代码在node.js中运行的结果是:立即超时,原因如下:程序运行时timer事件没有准备好,所以当第一次读取定时器队列时,queue为空,继续向下执行,在check队列中读取就绪事件,所以先执行immediate,再执行timeout,因为即使setTimeout的延迟时间不为0,node.js一般也会设置为1ms,所以当node准备EventLoop的时间大于1ms时,先输出timeout,再输出immediate,否则先输出immediateoutputtimeoutconstfs=require('fs')//读取文件fs.readFile('xx.txt',()=>{setTimeout(()=>{console.log('timeout')})setImmediate(()=>{console.log('immediate')})})上面代码的输出顺序一定是:立即超时,原因如下:setTimeout和setImmediate都写在I/O回调,表示处于poll阶段,然后是check阶段。因此,不管setTimeout准备多快(1ms),setImmediate都会先执行。本质上,它将从轮询阶段而不是Tick初始阶段开始执行。
