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

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

 

先来说结论:

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

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

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

哪些是macrotask?

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

些是microtask?

  1. Promise的then、catch、finally、all、race 
  2.  await(本质也是Promise)
  3.  queueMicrotask
  4.   MutationObserver
  5.   ……欢迎评论区补充

从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的回调。
来看看执行的过程:

  1.   先执行console.log('script start'),输出script start。
  2.   执行new Promise,并把后面then里的回调函数(微任务)放入event table。
  3.   执行new Promise 里的执行函数,执行到 console.log('promise1'),输出promise1,执行到resolve(),就把对应的微任务从event table排到微任务队列的队尾。
  4.   执行console.log('script end'),输出script end。
  5.   这里是一个分界线,一个执行栈里的一个宏任务执行完了,从微任务队列里一个个取出微任务放到执行栈执行,也就是把前面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
执行过程: 

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

参考连接

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

Event loop: microtasks and macrotasks (javascript.info)

Microtasks (javascript.info)

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值