IT技术之家

首页 > 前端

前端

浏览器中JS单线程机制与异步的实现_hehaonan~_浏览器单线程

发布时间:2023-11-28 21:10:35 前端 16次 标签:javascript 前端 开发语言 ecmascript
本文主要讨论浏览器环境上的js单线程机制和js异步的实现,关于其他环境下,比如node就暂时不讨论现来说结论:JS本身是没有办法真正实现异步的,异步的实现需要环境的支持(比如浏览器,因为浏览器是多线程的)。 JS有两个任务队列,分别是宏任务(macrotasks)队列和微任务(microtask)队列,JS也只有一个任务执行栈,执行栈只能放一个宏任务或一个微任务,执行完这个任务后清空执行栈。 浏览器有一个event table,用来确定宏任务或者微任务何时被添加到相应队列的队尾(个人觉得这个才是...

本文主要讨论浏览器环境上的js单线程机制和js异步的实现,关于其他环境下,比如node就暂时不讨论

?

先来说结论:

    JS本身是没有办法真正实现异步的,异步的实现需要环境的支持(比如浏览器,因为浏览器是多线程的)。JS有两个任务队列,分别是宏任务(macrotasks)队列和微任务(microtask)队列,JS也只有一个任务执行栈,执行栈只能放一个宏任务或一个微任务,执行完这个任务后清空执行栈。浏览器有一个event table,用来确定宏任务或者微任务何时被添加到相应队列的队尾(个人觉得这个才是JS异步的核心)。当用户打开一个页面的时候,JS的执行栈有一个宏任务,也就是一整个<script>代码,JS就会执行这个宏任务,当遇到宏任务时候,就会把这个宏任务注册到event table,JS继续执行下面的(同步)代码,这个宏任务什么时候被添加到宏任务队尾是由浏览器来控制,同样的遇到微任务,也会有类似的操作,当<script>的(同步)代码执行完后就会来一个个的处理微任务队列里的微任务,当微任务队列里的微任务执行完了,就会从宏任务队列取出一个宏任务,重复进行上述操作,这个过程叫Event Loop(事件循环)。粗略的讲,就是“一个宏任务———几个微任务”,这样子不断的循环。

图片来自:Event loop: microtasks and macrotasks (javascript.info)?

这里rander就把他当作一类普通的宏任务就可以了,执行的循序取决于渲染事件在宏任务队列里的顺序,并不一定是像图中一样的这么规律。?

哪些是macrotask?

    setTimeOut/setInterval函数I/O操作(addEventListener的回调等)?<script>代码界面的渲染?.……欢迎评论区补充

些是microtask?

    Promise的then、catch、finally、all、race??await(本质也是Promise)?queueMicrotask? MutationObserver? ……欢迎评论区补充

从setTimeOut来看macrotask


先上题目:

console.log('1')
setTimeout(()=>{
?? ?console.log('2');
},0);
console.log('3');

????????显然,这个的输出结果是1-3-2。
????????为什么不是1-2-3呢?setTimeout函数的时间设置不是0秒么?

????????前面说了setTimeOut是一个macrotask,这里的时间参数是0s(这里的0秒指的是从event table被压入宏任务队列的时间,不是真的过了0秒就马上执行),setTimeOut的回调函数在event table停了“0s”后马上被排到宏任务队列的队尾,但是这个宏任务要等执行栈里的宏任务执行完后再放入执行栈执行。

????????当前的宏任务执行完后,也就是输出1和2后,从宏任务队列里面拿出一个宏任务(setTimeOut的回调函数),放入执行栈并执行,然后输出3。
一个宏任务何时被放入event table取决于它什么时候被执行到(这里的“执行到”并不是指的宏任务被整个执行了,单纯的指V8引擎,碰到宏任务的标志,比如setTimeOut),一个宏任务什么时候被排到宏任务的队尾就要看触发条件(各种事件、定时器)什么时候触发,这里的setTimeOut就是什么时候时间到了,就被排到队尾。

????????这里的计时操作并不是JS的线程来干的,而是浏览器。

从promise来看microtask


这里不会promise的可以先去补一下,要不然有点吃力?

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

输出结果:script start-->promise1-->script end-->promise2
!!!这里要注意,promise的里面的执行函数是同步的,异步的是then的回调。
来看看执行的过程:

    ??先执行console.log('script start'),输出script start。??执行new Promise,并把后面then里的回调函数(微任务)放入event table。??执行new Promise 里的执行函数,执行到 console.log('promise1'),输出promise1,执行到resolve(),就把对应的微任务从event table排到微任务队列的队尾。??执行console.log('script end'),输出script end。??这里是一个分界线,一个执行栈里的一个宏任务执行完了,从微任务队列里一个个取出微任务放到执行栈执行,也就是把前面then的回调取出来,并执行,输出promise2。

复杂的情况下

function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3)
        });
    });
    
    new Promise((resolve, reject) => {
        console.log(4)
        resolve(5)
    }).then((data) => {
        console.log(data);
        
        Promise.resolve().then(() => {
            console.log(6)
        }).then(() => {
            console.log(7)
            
            setTimeout(() => {
                console.log(8)
            }, 0);
        });
    })
    
    setTimeout(() => {
        console.log(9);
    })
    
    console.log(10);
}
fn();

输出的结果:1,4,10,5,6,7,2,3,9,8
执行过程:?

    ?首先一开始<script>这个宏任务存在于执行栈里被执行,执行fn函数,执行到console.log(1),输出1。
      执行栈:栈顶【(1-33)】栈底宏任务队列:队头【】队尾微任务队列:对头【】队尾
    ?碰到setTimeOut就把一个宏任务(5-8行)注册到event table,然后因为这里的定时是0s,马上被压入宏任务队列的队尾。 执行栈:栈顶【(1-33)】栈底宏任务队列:队头【(5-8)】队尾微任务队列:对头【】队尾?碰到new Promise,救把这个Promise后面第一个then的回调(14-26行)作为一个微任务注册到event table。 执行栈:栈顶【(1-33)】栈底宏任务队列:队头【(5-8)】队尾微任务队列:队头【】队尾?执行Promise内部的执行函数,执行到console.log(4),就输出4,执行到resolve(5)就把event table里对应的微任务(14-26行)压入微任务队列队尾。 执行栈:栈顶【(1-33)】栈底宏任务队列:队头【(5-8)】队尾微任务队列:队头【(14-26)】队尾?又碰到setTimeOut,和第2步骤一样的操作。 执行栈:栈顶【(1-33)】栈底宏任务队列:队头【(5-8)(29-30)】队尾微任务队列:队头【(14-26)】队尾?执行 console.log(10),输出10,至此一个宏任务被执行完毕,执行栈是空的了。 执行栈:栈顶【】栈底宏任务队列:队头【(5-8)(29-30)】队尾微任务队列:队头【(14-26)】队尾?开始一个个执行微任务队列里的微任务。此时微任务队列只有一个微任务(14-26行),就是步骤4的那个微任务,执行这个微任务。 执行栈:栈顶【(14-26)】栈底宏任务队列:队头【(5-8)(29-30)】队尾微任务队列:队头【】队尾?执行console.log(data),这里的data因为是5,输出5。?碰到了Promise.resolve(),这个就和new Promise((resolve)=>{resolve()})是一样的,后面第一个then的回调函数(18-19行)先被注册到event table然后马上被压入微任务队尾,到这里步骤4的那个微任务(14-26行)就算是结束了。 执行栈:栈顶【】栈底宏任务队列:队头【(5-8)(29-30)】队尾微任务队列:队头【(18-19)】队尾?微任务队列多了一个步骤9压入的微任务(18-19行),取出这个微任务,放入执行栈马上执行,执行console.log(6),输出6,步骤9压入的微任务执行完毕。因为then会隐式返回一个Promise.resolve(undefined),所以会把后面第一个then的回调(20-25行)在event table注册,然后马上压入微任务(走个过场)。 执行栈:栈顶【】栈底宏任务队列:队头【(5-8)(29-30)】队尾微任务队列:队头【(20-25)】队尾?在微任务队列里取出步骤10(20-25行)压入的微任务,放入执行栈,执行console.log(7),输出7,碰到setTimeOut,参考步骤2,宏任务队列又多了一个宏任务(23-24行)。步骤10的微任务(20-25行)执行完毕。 执行栈:栈顶【】栈底宏任务队列:队头【(5-8)(29-30)(23-24)】队尾微任务队列:队头【】队尾?现在微任务队列为空,开始从宏任务取出宏任务,一个个执行。?取出步骤2注册的宏任务,执行console.log(2),输出2,注册一个微任务(7-8行),并马上压入微任务队列,步骤2注册的宏任务执行完毕,执行刚刚压入微任务队列的微任务(7-8行),执行console.log(3),输出3。 执行栈:栈顶【(5-8)】栈底宏任务队列:队头【(29-30)(23-24)】队尾微任务队列:队头【(7-8)】队尾?微任务队列空了,就从宏任务队列拿出步骤5注册的宏任务(29-30行),执行console.log(9),输出9。清空执行栈 执行栈:栈顶【(29-30)】栈底宏任务队列:队头【(23-24)】队尾微任务队列:队头【】队尾?微任务队列还是空的,取出步骤11注册的宏任务(23-24行),执行console.log(8),输出8。清空执行栈。 执行栈:栈顶【(23-24)】栈底宏任务队列:队头【】队尾微任务队列:队头【】队尾?此时,两个任务队列都是空的。执行完毕! 执行栈:栈顶【】栈底宏任务队列:队头【】队尾微任务队列:队头【】队尾

参考连接

前端干货:JS的执行顺序 - 简书 (jianshu.com)

Event loop: microtasks and macrotasks (javascript.info)

Microtasks (javascript.info)