美文网首页
JS同步异步与事件循环

JS同步异步与事件循环

作者: 郝同学1208 | 来源:发表于2020-10-16 10:02 被阅读0次

    文章序

    理解JS的单线程和同步异步逻辑、事件循环(event loop)是学习JavaScript进阶的必经之路,因此,本文章旨在让你一文彻底弄懂JS的单线程,异步,事件循环.

    单线程

    众所周知,浏览器的运行是多进程的,比如每多打开一个tab页,在任务管理器中就可以看到给浏览器多开辟了一个进程。每一个浏览器进程对应着多个线程,包括GPUI渲染线程、JS引擎线程、事件触发线程、定时器触发线程、http请求线程。因为JS的运行是单线程的,即代码由上自下运行,所以如果遇到循环次数多,或者其他耗时比较长的代码,便会阻塞.

    那么为什么JS是单线程的呢?举个很简单的例子,如果JS是多线程,在两个线程内同时对一个dom元素进行操作,那么,该dom元素该按照哪一个线程的要求进行渲染呢?这就是问题之所在。因此,JS选择了单线程.

    JS的同步异步

    就像刚刚说的那样,JS的运行是单线程的,如果出现比较耗时的代码,便会产生阻塞,直观体现就是用户的浏览器卡死、白屏等。因此,为了减少这种情况的出现,JS中出现了同步代码和异步代码.

    同步
    同步代码:即script标签内包裹的JS代码,包括通过script来import导入JS文件,也是属于同步代码,因此这里也就涉及到.js文件引入的优化问题,解决方案如AMD,CMD,ES6模块引入等,当然这就是另一个问题了.

    异步
    异步代码:典型的如Ajax请求,settimeout,setinterval等,我们知道为了防止浏览器卡死、白屏,JS引入了异步机制,那么同样的两段异步代码,先后执行顺序是怎样的?这里就要引出事件循环(Event loop)了.

    事件循环

    当代码/任务执行时,首先判断是同步任务还是异步任务,同步任务推入主线程,异步任务推入任务队列,主线程一直执行同步任务,直到所有同步任务执行完毕,主线程为空,则去任务队列中查看异步任务,将回调函数推入主线程执行,重复循环上述过程,就是事件循环.

    看下代码:

    setTimeout(()=>{
    console.log("定时器开始执行");
    })
     
    new Promise( function (resolve) {
        console.log("准备执行for循环了");
        for(var i=0; i<100; i++) {};
        resolve();
    }).then( () => console.log("执行then函数") );
     
    console.log("代码执行完毕");
    

    我们试着对执行结果分析:

    setTimeout 是异步任务,被放到任务队列;
    new Promise 是同步任务,被放到主进程里,直接执行打印 '马上执行for循环啦';
    .then里的函数是 异步任务,被放到任务队列;
     console.log('代码执行结束')是同步代码,被放到主进程里,直接执行;
    

    所以最后的执行结果是:[准备执行for循环---代码执行完毕---定时器开始执行---执行then函数 ],对吗?

    可是执行结果却是:[准备执行for循环---代码执行完毕---执行then函数---定时器开始执行],为什么会是这样呢?难道是异步任务执行的顺序不是前后顺序,而是另有规定?

    事实上,按照异步同步的划分并不准确而准确的方式应该是:
    macro-task(宏任务):script、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js);
    micro-task(微任务):Promise、MutaionObserver、process.nextTic(Node.js);

    事件循环

    按照上图这种分类方式,JS 的执行机制是:
    1、执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的[事件队列]里;
    2、当前宏任务执行完成后,会查看微任务的[事件队列],并将里面的微任务按先进先出依次执行完;
    重复以上2步骤,结合事件循环就是更为准确的JS执行机制了;

    按照这种执行机制,去分析第二段代码:

    setTimeout(()=>{
    console.log("定时器开始执行");
    }, 0)
    
    new Promise( function (resolve) {
        console.log("准备执行for循环了");
        for(var i=0; i<100; i++) {};
        resolve();
    }).then( () => console.log("执行then函数") );
    console.log("代码执行完毕");
    
    首先执行 script 下的宏任务,遇到 setTimeout 将其放到宏任务的队列里
    遇到 new Promise 立即执行,打印 "准备执行for循环" 
    遇到 then 方法,是微任务,将其放到微任务的队列里
    打印 "代码执行完毕" 
    本轮宏任务执行完毕,查看本轮的微任务,发现有一个 then 方法里的函数, 打印 "执行then函数" 
    到此,本轮的事件循环全部完成
    下一轮的循环里,先执行宏任务,发现宏任务的队列里有一个 setTimeout 里的函数,执行打印 "定时器开始执行"
    查看微任务队列,为空,结束本轮事件循环
    查看宏任务队列,为空,线程结束
    

    所以最后的执行顺序就是:[准备执行for循环-->代码执行完毕-->执行then函数-->定时器开始执行]

    最后再说一下setTimeout
    先看代码:

    setTimeout(function(){
        console.log('执行了');
     },3000);
    

    我们一般说这段代码,在3秒后执行setTimeout内的回调函数,但这种说法往往是不够严谨的,准确的解释是:3秒后,setTimeout函数会被推入事件队列,而事件队列内的任务,只有在主线程空闲时才会执行,所以,只有同时满足以下条件setTimeout内的函数才能被执行:
    1、3秒后;
    2、主线程空闲时;
    若主线程的执行任务很多,执行时间超过3秒,比如说5秒,那么setTimeout内的函数只能在5秒后执行了.

    相关文章

      网友评论

          本文标题:JS同步异步与事件循环

          本文链接:https://www.haomeiwen.com/subject/bmxzpktx.html