阅读 890

初步了解浏览器中的事件循环机制(event loop)

在我们讲解事件循环机制之前,我们先来了解了解JavaScript这门语言。

众所周知,JavaScript是一门单线程的语言,什么是单线程?可以简单理解为只能一件事一件事的干,只有一个执行流。那为什么不将JavaScript设置为多线程的呢,这效率不是巨高?

  • 因为JS可以修改dom结构,如果在JS执行的时候,UI线程还在工作,就可能导致不安全的渲染UI,得益于JS是单线程运行的,可以达到节省运行内存,节约上下文切换的时间(变快)。

所以为了避免复杂性,一开始JavaScript就设置成了单线程的。

同步任务和异步任务

我们都知道JavaScript中的事件分为同步任务异步任务。对于同步而言,是从上到下依次执行的,程序的执行顺序和任务的排列顺序是一致的。

image.png

这就是所谓的程序的执行顺序和任务的排列顺序是一致的。

异步呢?简单举个例子:比如说你在10点必须去街上买菜,但是现在才九点半。你会一直在那等着啥事不干,干等到10点?那必不可能,你会先着手去做其他事,等到了10点再去街上买菜。这个时候程序的执行顺序和任务的排列顺序不一致了。这便可理解为异步。

image.png

setTimeOut排在console.log(2)前面,但是他是延迟两秒执行(现在我们可以这么理解),执行时当读取到setTimeOut,不会等它执行然后再往下执行。如果等他执行完再去执行下面的代码,把延迟时间调到巨大,那完犊子了,想执行下面的代码得等到猴年马月。而是将它放到备忘录里去(这里指的是WebAPIs中的timer模块),先去执行下面的console.log(2),而console.log只是一个普通的方法,所以会立即执行输出 2 ,到这里,能立即执行的代码全执行完了,这时setTimeOut的延时时间也到了,又将它里面的东西放到代办事件里去(这里指的是任务队列task queue)。然后执行者会去代办事件里看看还有谁没执行,发现还有东西未执行,这时会去执行里面的console.log(1),输出 1 。

这里上个流程图瞅瞅,更直观些

setTimeOut.gif

到这我们简单了解了一下同步任务和异步任务的区别所在。

到这我们就能知道,JavaScript代码的执行过程中除了正常从上到下执行(函数调用栈)外,还会用到另一个东西(任务队列)去执行另一些代码,这整个执行过程可以简单的称为事件循环过程。

上面讲述执行过程时,谈到了一个名为任务队列的东西。

  • 我们都知道JavaScript是单线程的,拥有唯一的一个事件循环,但是呢,任务队列可以有好多个;
  • 任务队列分为宏任务(macro-task)和微任务(micro-task);
  • 宏任务包括:script(整体代码), setTimeout, setInterval, I/O, UI rendering;
  • 微任务包括: Promise, Promise .then, Object.observe(已废弃), MutationObserver(html5新特性);
  • 这里我们要注意一下setTimeOut和setInterval,进入任务队列的是他们里面的具体任务,而不是他们本身。他们被称为任务源

因为script也属于宏任务,而script表示一个整体的代码,这说明事件循环是从宏任务开始的

现在我们来说说这整个的执行流程:

简单来讲就是先执行宏任务,再去执行宏任务里面的微任务,当执行完这个宏任务里面的所有代码后,再去执行另一个宏任务,依次反复。 image.png

这里我们先来拿上面那个例子讲讲:

`
    setTimeOut(() => {
        consloe.log(1);
    })
    console.log(2);
`
复制代码

首先从上往下读代码:

  1. 首先script(整体代码)开始执行,全局上下文进入到调用栈中

image.png

  1. setTimeOut是一个宏任务,他的回调函数进入任务队列(setTimeOut是一个任务源)
  2. console.log(2)是同步任务,则直接在主线程中执行输出2

image.png 4. 这时主线程中的同步任务执行完毕 5. 去读取任务队列 6. 执行setTimeOut的的回调函数,输出 1 7. 执行完毕

上面只是前菜,让大家心里对事件循环机制有个底。

这里我们再来上个难点的例子:

    console.log('script start')
    setTimeout(function () {
        console.log('setTimeout')
    },0)
    new Promise(function (resolve) {
        console.log('promise1')
        resolve()
    }).then(function () {
        console.log('promise2')
    })
    console.log('script end');
    
复制代码

我们来理理上面代码的执行顺序:

1:首先script(整体代码)进入宏任务,全局上下文进入到调用栈中

image.png

2:script任务从上往下执行时,先遇到console.log('script start'),入栈执行

image.png

3: 然后继续向下执行,遇到setTimeout,他是一个宏任务源,将他里面的回调函数分配到宏任务队列中(这里我们为了便于直观的查看结果,只移动语句的输出结果)

image.png

4:继续往下执行遇到了Promise实例,这里我们要注意,这里是表示Promise构造函数,Promise构造函数是同步执行的,而.then是异步执行的(微任务),所以console.log('promise1')进入调用栈中执行并直接输出,而.then进入到微任务队列

这里我们可以参考一下这两篇

Promise构造器

使用 Promise(时序部分)

image.png

5:继续往下执行,读取到console.log('script end');没得想直接输出

6:最后,栈中的代码就执行完了,微任务中只有一个.then了,直接执行

7:所有的微任务就执行完了,第一轮循环就结束了,现在开启第二轮循环,从宏任务开始,宏任务中还有一个setTimeOut未执行,所以直接执行即可。

image.png 8:这是宏任务和微任务队列中没有了任务,所以全局上下文出栈,程序运行结束

到这这个例子就差不多解释完了,如果上面文字加图片还是不太能理解的话,来看看流程图吧!!

QQ录屏20210608173145.gif

结语

到这,我们的浏览器中的事件循环机制就讲解完了,这是我对于这个机制的理解。

最后再贴上一些例题吧,供大家思考 例一:

        console.log('script start')
        setTimeout(function(){
            console.log('setTimeout')
        },0)
        Promise.resolve().then(function(){
            console.log('promise')
        }).then(function(){
            console.log('promise2')
        })
        console.log('script end')
    
复制代码

例二:

       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)
    })
    setTimeout(()=>{
        console.log(6)
    })
    console.log(7)
复制代码
文章分类
前端
文章标签