[回顾]事件循环机制 (Event-loop)

1. JS的运行环境        

            js运行的环境我们称之为宿主环境,目前有三种运行环境,一种运行在浏览器(javaScript),一种运行在服务端(nodejs),另一种是运行在我们的客户端(比如Vscode客户端就是使用js写的),因此

            只要给js配备的相应的执行引擎,js可以运行在任何环境

image.png


2. 浏览器的宿主环境

image.png

     JS引擎线程


      • 负责执行执行栈的最顶层JS代码

      • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。


     GUI引擎线程

        

      • 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。

      • 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。


     事件监听线程 (DOM事件,window窗口事件等等)


      • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到事件队列的队尾,等待 JS 引擎处理。


     计时线程(setTimeout、setInterval计时器)


      • 开启定时器触发线程来计时并触发计时,计时完毕后,将计时器结束的回调函数添加到事件队列中,等待JS引擎空闲后执行,等待 JS 引擎处理。

      • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。



     网络线程(ajax网络请求)


      • http 请求的时候会开启一条请求线程。

      • 请求完成有结果了之后,将请求的http回调函数添加到任务队列中,等待 JS 引擎处理。




3. 事件队列(任务队列/消息队列)


      事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

        

    • 宏任务(队列):macroTack、计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列

    • 微任务(队列):MutationObserver,Promise产生的回调进入微队列

MutationObserver 用于监听某个DOM对象的变化

当执行栈清空时、JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,执行宏任务


4. 事件循环(Event Loop)


        事件循环分为三个部分,分别由 浏览器宿主,web api 与 事件队列(也称任务队列)组成


5. 事件循环机制


  • 执行栈

    由于JavaScript 引擎是单线程,同一时间只能执行一个任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务,因此这些任务被排队在一个单独的地方。这个地方被称为执行栈

image.png

        执行栈是一个后进先出数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈,函数调用之前,创建执行环境,然后push到执行栈;函数调用之后,

        销毁执行环境,并从执行栈顶部推(pop)出去


  • 同步任务、异步任务

    同步任务:当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。 一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

    异步任务:js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当这个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列事件队列(事件队列是先进先出的数据结构),被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕。异步函数的执行事件,会被宿主环境控制。

image.png


  • 事件循环(Event Loop)

    主线程处于闲置状态时,主线程会去查找事件队列是否有任务。 如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

image.png


6. 举个栗子

 栗子1(同步任务)

    <script>        
        function a() {
            console.log("a")
            b();
        }        function b() {
            console.log("b");
            c();
        }        function c() {
            console.log("c")
        }

        console.log("global");
        a();
    </script>

运行流程:

在JS引擎代码执行js代码之前<script>,首先会初始化一个全局执行上下文(也称执行环境),并push到栈顶,接下来开始在全局执行环境下,执行代码,首先将三个函数定义,提取到全局上下文里去,然后继续执行,等执行到console.log('c')之后,会调用console对象里的一个log函数,(调用任何一个函的时候,都会为这个函数创建一个函数执行上下文),然后创建log的执行上下文,并push入栈,然后就可以运行这个函数了,(始终记住js引擎永远执行的是执行栈的最顶部),运行该函数后,控制台输出一个"global",然后该函数运行结束,销毁该执行上下文,并从栈顶被推出(出栈),此时又回到了全局执行环境之下继续执行,此时又遇到了a函数的调用,调用该函数,创建一个a的函数执行上下文,并push入栈,然后开始执行a函数里面的代码,在a函数里又遇到里console对象里的log函数,又为这个log函数创建一个log的函数执行上下文,并push到栈顶,然后运行log函数,控制台输出a,之后log函数执行结束,销毁log的上下文,回到a的函数执行上下文上,继续运行,然后又发现b函数的调用,此时创建b的函数执行上下文,并push入栈,然后运行b函数,在b函数里发现console对象里的log函数的调用,然后继续为这个log函数创建log的函数执行上下文,并push入栈,然后运行该log函数并在控制台输出"a",此时该log函数执行结束,销毁其log的上下文,并从栈顶被推出(出栈pop),此时又回到了b的函数执行上下文环境下执行,继续运行,发现了c函数的调用,然后为c函数创建C的函数执行上下文,并push入栈,然后开始执行c里面的代码,执行代码过程中,发现了console对象里的log函数被调用,又为这个log函数创建一个log的函数执行上下文,并push到栈顶,在该log环境里,运行该函数,输出"c",之后该log函数运行结束,销毁其log上下文,并从栈顶被推出(出栈pop),然后回到c的执行环境中,此时已经没有可以运行的数据了,则c运行结束,销毁c的函数执行上下文,并从栈顶被推出(出栈pop),然后回到b的执行环境之中,c的调用结束之后,b也没有可以执行的代码,此时b函数也运行结束,则销毁b的函数执行上下文,并从栈顶被推出(出栈pop),此时回到a的执行环境执行,a函数在b函数的调用结束后也没有可再执行的代码,因此a函数也执行结束,销毁a的函数执行上下文,并从栈顶被推出(出栈pop),最终执行权又回到了全局执行环境里,在全局执行环境里,发现已经没有可运行的代码了,然后也销毁全局执行上下文,并从栈顶推出(出栈pop),此时执行栈里面都为空,如果事件队列有事情,则会把执行事件队列里的代码。

Rec 0005.gif



栗子2(异步任务-DOM事件监听)

    <div>
        <button id="btn">点击</button>
    </div>

    <script>
        document.getElementById("btn").onclick = function A() {
            console.log("按钮被点击了");
        }
    </script>

运行流程:

首先:在JS引擎(主线程)代码执行js代码之前<script>,会初始化一个全局执行上下文(也称执行环境),并push到栈顶,接下来开始执行在全局执行环境下的代码,首先,调用了document下面的getElementById()函数,创建一个getElementById的上下文,并push到栈顶,(始终记住,JS引擎永远执行的是执行栈的最顶部),然后JS引擎进入getElementById的上下文(执行环境),运行该函数,并获得一个DOM对象,得到该DOM对象后,此时该函数也运行结束,则销毁该的函数执行上下文,并从栈顶被推出(出栈pop),此时执行权回到全局执行环境,继续执行,然后给获取到的DOM注册onlick点击事件,并赋值为一个函数,接着,JS引擎会通知浏览器宿主环境的事件监听线程去是监按钮点击事件,然后去运行函数A,之后,全局上下文发现已经没有代码可以执行了,然后全局上下文结束,销毁全局执行上下文,并从栈顶被推出(出栈pop),此时执行栈为空。由于事件监听线程本身没有执行代码的权利,因此会在监听到按钮被点击过后,并不会直接执行代码,而是会把函数A放入到事件队列中去(宏任务)等待执行,但为什么要把函数先放回到事件队列中去呢?原因是如果直接将被点击的回调函数如果到执行栈中,如果此时某个函数正在被执行,你又把函数A放入到执行栈中,执行栈就会蒙蔽了,它不知道要先执行哪个,这样就会造成了代码堵塞,(JS语言是非阻塞的)因此我们需要将函数A放入到事件队列中等待被执行栈执行,当执行栈为空的时候,JS引擎会去查找事件队列是否有任务。此时,JS引擎发现事件队列中有函数A存在,然后把这个函数A放入执行栈的最顶部,开始执行其中的同步代码,在A函数里遇到里console对象里的log函数,然后为这个log函数创建一个log的函数执行上下文,并push到栈顶,然后JS引擎进入getElementById的上下文(执行环境),运行log函数,控制台输出"按钮被点击了",之后log函数执行结束,销毁log的上下文,并从栈顶推出(出栈pop),然后执行权回到函数A执行上下文上,此时函数A发现已经没有代码可以执行了,然后函数A结束,销毁函数A的上下文,并从栈顶被推出(出栈pop),此时,万物归于沉静!

Rec 0008~1.gif


 MutationObserver 用于监听某个DOM对象的变化[在GUI渲染线程里,由HTML5提出]

    <script>        
        let count = 1;
        const ul = document.getElementById("container");
        document.getElementById("btn").onclick = function A() {            
            setTimeout(function C() {
                console.log("添加了一个li")
            }, 0);
            var li = document.createElement("li")
            li.innerText = count++;
            ul.appendChild(li);
        }

        //监听ul
        const observer = new MutationObserver(function B() {
            //当监听的dom元素发生变化时运行的回调函数
            console.log("ul元素发生了变化")
        })
        //监听ul
        observer.observe(ul, {
            attributes: true, //监听属性的变化
            childList: true, //监听子元素的变化
            subtree: true //监听子树的变化
        })
        //取消监听
        // observer.disconnect();
    </script>


评论 抢沙发

表情
首页上一页12下一页尾页