# 1.Js执行机制

# 浏览器常驻的线程

js引擎线程(解释执行js代码、用户输入,网络请求)

GUI线程(绘制用户页面、与js主线程是互斥的)

http网络请求线程(处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)

定时触发器线程(setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)

浏览器事件处理线程(click、mouse等交互事件发生后将这些事件放入事件队列中)

JS引擎线程和GUI引擎线程————互斥

js可以操作DOM元素,进而会影响到GUI的渲染结果,因此js引擎线程与gui渲染线程是互斥的。也就是说当js引擎线程处于运行状态时,gui渲染线程将处于冻结状态。

    <button id="button">test</button>
    <script>

        var button = document.getElementById('button');
        console.log(button);
        button.onclick = function () {
            button.style.fontSize = '100px';
            button.style.color = 'red';
            for(let index = 0; index < 1000000; index ++) {
                button.innerText = index;
            }
        }
         
    </script>
在上述代码中,即使style的样式写在innerText之前,但因为style样式所在的域是click function,所以在整个onclick function函数执行完成之前,GUI的线程都会处于冻结状态。因此button的样式(fontSize、color、innerText)会等到函数执行完之后,才一起、同时改变。
js为何是单线程?

假如js是多线程,同一个时间一个线程想要修改DOM,另一个线程想要删除DOM,问题就会复杂许多,浏览器不知道听谁的,如果引入“锁”的机制,就又回到其他语言的尴尬困境了。
大量数据操作怎么办?

单线程计算能力有限,大量数据需要计算渲染的话,我们可以配合后端进行操作,比如VUE和nodejs配合,也就是SSR技术。

# js运行逻辑

JavaScript是基于单线程运行的,同时又是可以异步执行的,一般来说这种既是单线程又是异步的语言都是基于事件来驱动的,恰好浏览器就给JavaScript提供了这么一个环境。

上图内容用文字表述的话: 同步和异步任务分别进入不同的“场所”,同步的进入主线程,异步的进入Event Table并注册函数 当指定的事情完成时,Event Table会将这个函数移入Event Queue 主线程内的任务执行完毕为空,回去Event Queue读取对应的函数,进入主线程执行,上述过程会不断重复,也就是常说的Event Loop(事件循环)

同步任务

<script>
    function outer(ot){
        function inner(it) {
            console.log(it);
        }
        inner(20);
        console.log(ot);
    }
    outer(10);
</script>
// 20
// 10
  1. 代码没有执行的时候,执行栈为空栈

  2. outer 函数执行时,创建了一帧,帧中包含了形参、局部变量 (预编译过程) ,把帧压入栈中

  3. 执行 outer 函数内代码,执行 inner 函数

  4. 创建新帧,同样有形参、局部变量,压入栈中

  5. outer 函数执行完毕,弹出栈

  6. inner 函数执行完毕,弹出栈

  7. 执行栈为空(执行栈相当于js主线程

异步任务

$.ajax({
    url:'xxx.xxx.com',
    data:{},
    success:function(data) {
        console.log(data);
    }
})
console.log('run')

0.Ajax进入Event Table,注册回调函数success

1.执行console.log('run')

2.Ajax事件完成,http网络请求线程把success任务放入Event Queue中

3.主线程(调用栈)读取任务,执行success

任务队列

由于JS是一个单线程的语言,就需要一个专门来管理任务执行的队列,只有前一个任务执行完了,才会接着去执行下一个任务。但是有些任务耗费的时间很长,如果是cpu一直在忙碌在干事情,那倒也没什么,最怕的是此时cpu是处于空闲状态的。比如I/O操作,或者Ajax请求,有时候我们发出一个ajax到后端请求数据到浏览器接受到后端返回的数据,这段时间比较长。此时若是cpu一直处于空闲等待的状态的话,那岂不是会浪费很多性能。所以设计者就考虑不妨将这些需要等待的任务挂起,去执行后面的任务,等到所请求的数据返回或者I/O操作结束有了结果之后再回过头去执行刚刚被挂起的任务。

因此根据上述我们又可以将任务分为两类,一种是同步任务,一种是异步任务。我们将同步任务放在浏览器的主线程之上,只有前面一个任务执行完成之后才会执行后面的任务,异步任务是指 不进入主线程,而进入任务队列的任务。只有主线程里的同步任务都执行之后(主线程里没其他任务了),才会执行任务队列里的异步任务。

具体来说异步任务的执行机制如下:

1:有一个主线程用于执行一些同步任务,形成一个执行栈。

2:与此同时还有一个任务队列,当异步任务有了运行结果的时候,就会在任务队列中放置一个事件。

3:一旦主线程上的同步任务都执行完了,系统就会依次读取任务队列上的任务。而此时正处于“等待”状态的异步任务也就结束等待了,开始进入执行栈中,开始执行。

4:主线程不断反复的重复上面第三部。

一旦主线程上面的任务运行完之后就会从任务队列中读取新的任务继续运行。这就是javascript的运行机制,整个过程不断循环反复直至所有任务全部运行结束。

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

微队列,microtask,也叫jobs

另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  1. process.nextTick (Node独有)

  2. Promise

  3. Object.observe

  4. MutationObserver(用来监视 DOM 变动。比如节点的增减、属性的变动、文本内容的变动。) (注:这里只针对浏览器和NodeJS)

宏队列,macrotask,也叫tasks

一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  1. setTimeout 、setInterval

  2. setImmediate (Node独有)

  3. requestAnimationFrame (浏览器独有)

  4. requestAnimationFrame在MDN的定义 (opens new window)为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。严格来说,这个api不属于宏任务也不属于微任务,他的触发时间位于宏任务和微任务之间。

  5. I/O

  6. UI rendering (浏览器独有)

    我们先来看一张图,再看完这篇文章后,请返回来再仔细看一下这张图,相信你会有更深的理解。 在这里插入图片描述

    这张图将浏览器的Event Loop完整的描述了出来,我来讲执行一个JavaScript代码的具体流程:

    1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
    2. 全局Script代码执行完毕后,调用栈Stack会清空; 
    3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
    4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。
    5. 注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
    6. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
    7. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行; 执行完毕后,调用栈Stack为空;
    8. 重复第3-7个步骤; 
    9. 重复第3-7个步骤; ......
       **可以看到,这就是浏览器的事件循环Event Loop**
    

    这里归纳3个重点:

    1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
    2. 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空; 
    3. 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。