JS执行机制(eventloop)

js是单线程

js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM

假设如果js同时有两个线程,同时对同一个DOM进行操作,这时浏览器应该听哪个线程的,如何判断优先级?

为了避免这种问题,js必须是一门单线程语言

js执行机制

执行栈

当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,然后执行栈再去任务队列读取、执行。等待主线程读取,遵循先进先出原则。

执行任务队列中的某个任务,这个被执行的任务就称为执行栈

主线程

主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。

主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

js 异步执行的运行机制。

console.log('开始执行1');

setTimeout(() => {
    console.log('2秒后执行的内容');
}, 2000);

setTimeout(() => {
    console.log('0秒后执行的内容');
}, 0);

console.log('结束执行4');//1 4 0 2

先将所有的同步代码压入执行栈依次执行,期间将异步代码放入任务队列。同步代码执行完毕后,在回调函数队列中根据定时器的时间来依次执行相应的代码,延时少的先执行,延时多的后执行,否则默认从队头压入执行栈

  • 所有任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  • 主线程不断重复上面的第三步


异步任务的宏与微

异步

加载图片音乐之类占用资源大耗时久的任务,就是异步任务

JS 的异步是通过回调函数实现的。

一般而言,异步任务有以下三种类型:

  1. 普通事件,如clickresize
  2. 资源加载,如loaderror
  3. 定时器,包括setIntervalsetTimeout

异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)。

 

不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

图片.png

Event Loop(事件循环)

浏览器的事件循环由一个宏任务队列+多个微任务队列组成。

首先,执行第一个宏任务:全局Script脚本。产生的的宏任务和微任务进入各自的队列中。执行完Script后,把当前的微任务队列清空。完成一次事件循环

接着再取出一个宏任务,同样把在此期间产生的回调入队。再把当前的微任务队列清空。以此往复。

宏任务队列只有一个,而每一个宏任务都有一个自己的微任务队列,每轮循环都是由一个宏任务+多个微任务组成。

图片.png

例题如下:

     setTimeout(function () {
       console.log(1)
    })
​
     new Promise(function (resolve, reject) {
       console.log(2)
       resolve(3)
    }).then(function (val) {
       console.log(val) // 微任务1
    })
​
     new Promise(function (resolve, reject) {
       console.log(4)
       resolve(5)
    }).then(function (val) {
       console.log(val) // 微任务2
    })
​
     console.log(6)

第一步:宏任务A

(直接整体js代码就是第一个宏任务)

1、script中的同步代码依次执行

2、第一个new Promise中的console.log(2),得到2

3、第二个new Promise中的console.log(4),得到4

4、外面的script同步代码console.log(6),得到6

此次宏任务执行完毕;

第二步:所有可执行的微任务

1、then后面的属于Promise微任务,全部依次执行,得到3, 5

第三步:宏任务B

1、执行新的宏任务,得到1

最后,可以很轻松猜出或者说是写出正确答案:2,4,6,3,5,1

例题2:

console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})


setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})

完整的输出为1,7,6,8,2,4,3,5,9,11,10,12

eventloop再学习总结:

  1. JS引擎线程:负责JS的解析以及所有同步异步任务的执行。维护一个执行栈,先逐个处理同步代码,当遇到异步任务,就会借助事件触发线程
  2. 事件触发线程:事件触发线程维护一个任务队列,其中又分为微任务队列和宏任务队列。任务队列里的异步任务被触发时,该任务就会被放到对应任务队列的队尾,等待js引擎线程的执行栈清空,再从任务队列中取任务进行处理。
  3. 计时器线程:setTimeoutsetInterval所在的线程

宏任务、微任务和事件循环机制

事件循环机制:

  1. JS引擎逐行扫描js代码,遇到同步任务加入执行栈。
  2. 遇到异步任务如setTimeout发送ajax请求等的异步任务,就交给事件触发线程。当该异步任务被触发,就放到任务队列的末尾。该线程维护一个微任务队列和一个宏任务队列
  3. JS引擎将执行栈里的同步任务执行完之后,就去从宏任务队列队首取一个宏任务,和对应的微任务到执行栈里,直到微任务队列为空。注意宏任务是一次事件循环取一个,而微任务是一直取一直取直到任务队列为空
  4. 再取一个宏任务,重复3的过程。

microtast(微任务):Promise.then, process.nextTick
macrotask(宏任务):script整体代码、setTimeout、 setInterval等

特别注意

当微任务嵌套微任务时,内层微任务先执行,外层微任务后执行

Promise和async中的立即执行

我们知道Promise中的异步体现在thencatch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

await做了什么

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}
等价于

async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

变式一

在第一个变式中我将async2中的函数也变成了Promise函数,代码如下:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

变式二

在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:

async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
	setTimeout(function() {
		console.log('setTimeout2')
	},0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');


script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

变式三

变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

无非是在微任务那块儿做点文章,前面的内容如果你都看懂了的话这道题一定没问题的,结果如下:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout

async function test() {
  return new Promise((resolve)=>{
    setTimeout(() => {
        console.log('2秒后test');
        resolve('test 1000');
    }, 2000);
  })
}
function fn() {
    console.log('fn');
  return 'fn';
}

async function next() {
    let res0 = await fn(),
        res1 = await test(),
        res2 = await fn(),
        res3 = await test();
        console.log('被阻塞')
  

}
console.log('1')
next(); // 
setTimeout(() => {
  console.log('settime');
  
}, 2000);
setTimeout(() => {
  console.log('settime2');
  
}, 2000);
console.log('1')



输出:
1
fn
1//整体代码同步任务

settime//2秒后几乎同时输出set,set2,2秒后test,fn,先进入宏任务队列 在队头
settime2//在队尾
2秒后test
fn

2秒后test//4秒后输出
被阻塞

建议精读这一次,彻底弄懂 JavaScript 执行机制便可

参考:

这一次,彻底弄懂 JavaScript 执行机制

Js 的事件循环(Event Loop)机制以及实例讲解

setTimeout+Promise+Async输出顺序?很简单呀!

阅读剩余
THE END