所谓单线程,就是 同一个时间只能作一件事。JavaScript从诞生之初就是做为浏览器的一种脚本语言,其主要用途是与用户互动,以及 操做DOM,而这就决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程, 一个线程在某个DOM节点上添加内容, 另外一个线程删除了这个节点,这个时候浏览器就不知道该如何处理了, 究竟是应该在节点上添加内容仍是应该删除这个节点呢?
虽然为了利用CPU的多核计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是 子线程彻底受主线程控制, 且不得操做DOM,因此, 这个新标准并无改变JavaScript单线程的本质。
任务队列是指 task queue,因为JavaScript是单线程的,因此 全部任务必须进行排队依次进行处理。而任务又分为 同步任务和 异步任务, 同步任务直接进入主线程中进行排队, 异步任务则进入任务队列中进行排队,同步任务是在 主线程的调用栈中执行的, 只有主线程的调用栈被清空的时候, 才会执行任务队列中的任务,这也就是所说的 JavaScript的运行机制。
一般异步操做都会进入到任务队列中,好比setTimeout()、setInterval(),这里须要注意的就是 浏览器是多线程的,主要为 UI渲染线程、 JS引擎线程、 GUI线程(主要用于处理事件交互),其中, JS引擎线程和UI渲染线程是互斥的,即, 若是JS引擎主线程在执行,那么UI将没法进行渲染,由于 JS引擎线程是能够进行DOM操做的,只有互斥才能保证不会出现UI引擎在渲染的同时,JS引擎线程同时在修改DOM,如页面中有一个按钮,点击按钮后会开始一段耗时比较长的计算,这里要求实现点击按钮后按钮文字显示"计算中",计算完成后,按钮文字显示"计算完成"。
<body> <button id="btn">点我</button> </body>
let btn = document.getElementById("btn"); function long_running() { console.log("long_running"); var result = 0; for(var i = 0; i < 1000; i++) { for(var j= 0; j < 1000; j++) { for(var k=0; k< 1000; k++) { result = result + i + j + k; } } } btn.innerHTML = "计算完成"; } btn.addEventListener("click", (e) => { btn.innerHTML = "计算中..."; long_running(); });
运行如上代码,咱们能够发现点击按钮后并无先变成"计算中",而后再变成"计算完成",而是点击以后无变化,而后等计算完成后直接变成了"计算完成"。由于btn.innerHTML = "计算中..."; 是进行DOM操做, 使用的UI渲染线程,此时, JS引擎线程调用栈还未清空(还须要往下执行js),因此 还不能当即执行,而后执行long_running(),long_running()不是异步任务,不进入到任务队列中,直接进入到主线程的调用栈中执行,因为耗时比较长,等long_running()执行完成后,主线程调用栈被清空,UI渲染引擎开始执行,因此直接显示"计算完成"了,要实现上述效果,咱们须要给long_running()添加一个延时,让其进入到任务队列中,不要占用主线程调用栈,让btn.innerHTML = "计算中..."先执行,再进行计算。如:
btn.addEventListener("click", (e) => { btn.innerHTML = "计算中..."; setTimeout(() => { long_running(); }, 0); });
添加延时后,long_running();也进入到了任务队列中,因此会先执行btn.innerHTML = "计算中...";再执行long_running();等计算完成后再更新为"计算完成"。
异步任务又分为 宏认为和 微任务。宏任务包括 总体代码script, setTimeout, setInterval, 宏认为进入宏任务队列,而且 宏任务队列能够有多个;微任务包 Promise的then(回调), process.nextTick, 微任务进入微任务队列,而且 微任务队列只有一个,当宏任务队列的中的任务所有执行完之后,会查看微任务队列中是否有微任务,好比 在执行宏任务的时候产生了微任务,那么 会先执行微任务队列中的全部微任务,若是微任务队列中没有微任务,那么直接执行下一个宏任务队列,重复执行以前的执行步骤,从而造成事件环。
① 示例1promise
setTimeout(() => console.log('setTimeout1'), 0); //1宏任务 setTimeout(() => { //2宏任务 console.log('setTimeout2'); Promise.resolve().then(() => { console.log('promise3'); Promise.resolve().then(() => { console.log('promise4'); }) console.log(5) }) setTimeout(() => console.log('setTimeout4'), 0); //4宏任务 }, 0); setTimeout(() => console.log('setTimeout3'), 0); //3宏任务 Promise.resolve().then(() => {//1微任务 console.log('promise1'); })
首先总体代码首先产生了一、二、3三个宏任务,进入宏任务队列,而后执行到最后一行Promise的时候产生了一个微任务,进入微任务队列,由于 总体代码是一个宏任务, 宏任务结束后会检查微任务队列中是否有任务,发现有一个,因此首先输出promise1,微任务清空后,接着执行下一个宏任务,虽然一下产生了三个宏任务,可是因为时间都是0,因此这三个宏任务其实至关因而一个大的宏任务,能够合在一块儿,如:
setTimeout(() => { console.log('setTimeout1'); // 宏任务1 console.log('setTimeout2'); Promise.resolve().then(() => { console.log('promise3'); Promise.resolve().then(() => { console.log('promise4'); }) console.log(5) }) setTimeout(() => console.log('setTimeout4'), 0); console.log('setTimeout3') // 宏任务3 }, 0);
因此接着执行这个大的宏任务,输出setTimeout1,setTimeout2,setTimeout3,而后执行宏任务的过程当中产生了一个微任务和一个宏任务,因此接着执行这个微任务,输出promise3,5,而后执行微任务的过程当中又产生了一个微任务,而后继续执行微任务输出promise4,此时微任务清空完毕,执行最后一个宏任务,输出setTimeout4。
②示例2浏览器
new Promise((resolve,reject)=>{ console.log("promise1") resolve() }).then(()=>{ console.log("then1-1") new Promise((resolve,reject)=>{ console.log("promise2") resolve() }).then(()=>{ console.log("then2-1") }).then(()=>{ console.log("then2-2") }) }).then(()=>{ console.log("then1-2") })
首先执行总体代码,当即输出promise1,而后产生了一个微任务,没有宏任务,接着执行产生的微任务,输出then1-1,promise2,执行微任务的过程当中又产生了一个微任务,进入到微任务队列,外层第一个then执行完成,接着执行第二个then又产生了一个微任务,因而添加到微任务队列,此时微任务队列中有两个微任务了,即内层的第一个then,和外层的第二个then,故依次输出then2-一、then1-2,在执行内层第一个then的过程当中又产生了一个微任务,继续添加到微任务队列,而后输出then2-2
首先promise.then和process.nextTick属于微任务,setTimeout和setImmediate属于宏任务,而且 process.nextTick的优先级要高于promise.then, setTimeout的优先级高于setIImmediate。
setImmediate(function(){ // 宏任务1 console.log(1); },0); setTimeout(function(){ // 宏任务2 console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ // 微任务1 console.log(5); }); console.log(6); process.nextTick(function(){ // 微任务2 console.log(7); }); console.log(8);
首先执行总体代码,产生了两个宏任务,而后建立Promise执行同步代码输出3和4,此时产生了一个微任务1,接着输出6,而后再产生了一个微任务2,接着输出8,此时总体代码执行完毕,而后检测微任务队列并执行,此时微任务队列中有两个,虽然微任务2后面添加进去,可是微任务2是由process.nextTick建立具备更高优先级,因此先执行微任务2,依次输出7和5,接着再执行宏任务,因为setTimeout比setImmediate具备更高优先级,因此先执行宏任务2,依次输出2和1,故最终结果为三、四、六、八、七、五、二、1。
① 示例1多线程
console.log(1); setTimeout(function() { // 宏任务1 console.log('2'); process.nextTick(function() { // 微任务3 console.log('3'); }); new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { // 微任务4 console.log('5'); }); }, 0); process.nextTick(function() { // 微任务1 console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { // 微任务2 console.log('8') }); setTimeout(() => { // 宏任务2 console.log('9'); process.nextTick(function() { // 微任务5 console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { // 微任务6 console.log('12'); }); }, 0);
首先执行总体代码,输出1,并产生宏任务1,接着产生一个微任务1,接着建立Promise执行同步代码输出7,并产生微任务2,最后产生一个宏任务2;接着状况微任务队列,微任务队列中有两个任务,故依次输出6和8;而后再执行宏任务队列,因为宏任务1和2时间都是0,因此能够看作是一个大的宏任务,先输出2,并产生微任务3,接着建立Promise执行同步代码输出4,而后产生微任务4,继续执行宏任务2,输出9,产生微任务5,接着建立Promise执行同步代码输出11,并产生微任务6,此时宏任务1和2执行完毕,接着须要清空微任务队列,微任务队列中有三、四、五、6,因为process.nextTick优先级高于Promise.then,因此先输出3和10,而后再输出5和12,故最终输出结果为一、七、六、八、二、四、九、十一、三、十、五、12
② 示例2异步
async function async1() { console.log('async1 start') await async2(); console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(() => { console.log('setTimeout0') },0) setTimeout(() => { console.log('setTimeout3') },3) setImmediate(() => { console.log("setImmediate"); }); async1(); new Promise((resolve) => { console.log("promise1"); resolve(); console.log("promise2"); }).then(() => { console.log("promise3"); }); process.nextTick(() => { console.log("nextTick"); }); console.log("scritp end.");
这道题主要考察的是async函数的执行原理, async函数会返回一个Promise对象,当函数执行的时候,一旦遇到await就会当即返回,可是要等到await后的代码执行完成后才能回到主线程,即接着执行函数外的同步代码,函数外的同步代码执行完成后再回到async函数内接着执行,而且之间若是产生了微任务,那么须要先清空微任务。
首先执行总体代码,输出 script start,而后setTimeout0、setTimeout三、setImmediate进入到宏任务队列,接着执行async1函数,输出 async1 start,而后遇到await,async1函数当即返回,可是还要等到await以后的代码async2执行完毕,async2执行完成输出 async2,此时回到主线程继续执行,即执行Promise中的同步代码,输出 promise1、 promise2,而后产生一个promise3微任务,接着nextTick也进入到微任务对列,接着输出 scritp end,此时主线程执行完毕,即主线程调用栈已经被清空,接着检测是否有微任务队列,发现有,开始执行微任务队列,产生了nextTick和promise3两个微任务,而且nextTick优先级更高,依次输出 nextTick、 promise3,此时再次回到async1()执行剩余的代码,输出 async1 end,接着再执行宏任务队列中的代码,setTimeout0和setImmediate时间都是0,而且setTimeout0优先级更高,依次输出 setTimeout0、 setImmediate,最后输出setTimeout3。