JS执行原理

80 阅读6分钟

今天在回看以前笔记的时候,发现自己写的笔记有些看不懂了!,js执行机制也是重点之一了,今天赶紧来复习一下~

微信图片_20220622143119.png

首先,我们都知道js是动态类型语言,是在运行时编译。

微信图片_20220622174232.png V8引擎是一个接收JavaScript代码,编译JS代码然后执行的C++程序。编译后的代码,可以再多种操作系统,多种处理器上运行。V8要负责以下工作:编译和执行JS代码、处理调用栈、内存的分配、垃圾的回收。

那么V8是如何编译和执行JS代码的?

  • 解析器:将JS代码解析成抽象语法树AST
  • 解释器负责将AST解释成字节码bytecode,同时解释器也有直接解释执行bytecode的能力;
  • 编译器负责编译出运行更加高效的机器代码

那么V8的优点有哪些呢? 1.函数只声明不调用时,不会被解析成抽象语法树AST。 2.函数只调用一次,会解释为字节码bytecode直接被执行。 3.函数调用多次,可能会被标记被热点函数,可能会被编译为机器代码。

调用栈和队列

栈: 先进后出

堆: 先进先出

调用栈是JS引擎追踪函数执行流程的一种机制,当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体又调用了哪个函数。它采用了先进后出的机制来管理函数的执行。

调用栈是如何管理执行顺序的:函数的声明是不会放入栈中的,调用栈,顾名思义一定是被调用的函数才会入栈。

javaScript的执行环境是一个单线程,这就意味着JS环境只有一个调用栈,如果调用栈中的某个函数执行需要消耗大量时间的话,那么就会导致调用栈被阻塞无法入栈和出栈。页面的布局绘制和JS执行都是在一个主线程里。如果JS执行迟迟不归还主线程的话,就会影响页面的渲染就可能会导致页面出现卡顿的现象。也就会严重影响用户的体验。优化这个问题的方案就是使用事件循环和异步回调。

回调函数

众所周知,JS是单线程的,因为多个线程改变DOM的话会导致页面紊乱,所以设计为一个单线程的语言,但是浏览器是多线程的,这使得JS同时具有异步的操作,即定时器,请求,事件监听等,而这个时候就需要一套事件的处理机制去决定这些事件的顺序,即Event Loop(事件循环),下面会详细讲解事件循环,只需要知道,前端发出的请求,一般都是会进入浏览器的http请求线程,等到收到响应的时候会通过回调函数推入异步队列,等处理完主线程的任务会读取异步队列中任务,执行回调。

EventLoop

异步和多线程的实现是通过事件循环机制实现的,即EventLoop (microtask Queue)

Event Loop由三部分组成:1、调用栈(call stack),2、消息队列(Message Queue),3、微任务队列(microtask Queue)

Event Loop开始时会从全局开始一行一行执行代码,遇到函数调用,则会将其压入到调用栈中(被压入的函数叫做帧(Frame))执行里面的代码,当函数返回时,会被弹出调用栈。

示例一:

      function func1() {
        console.log(1);
      }
      function func2(params) {
        console.log(2);
        func1();
        console.log(3);
      }
      func2();

上面示例一中的代码执行顺序是:函数func2调用,被压入调用栈,执行里面的代码,执行console.log(2); 接着调用函数func1,func1被压入调用栈,执行内部代码 console.log(1);然后函数返回,调用栈中的函数1弹出,继续执行函数func2中剩余代码console.log(3);然后函数func2返回弹出调用栈。 所以最后打印结果为2 1 3

那像js里面的异步操作比如fetch、setTimeOut、setInterval的回调函数等,会进入到消息队列中,称为“消息”。消息会在调用栈清空的时候执行

示例二

      function func1() {
        console.log(1);
      }
      function func2() {
        setTimeout(() => {
          console.log(2);
        }, 0);
        func1();
        console.log(3);
      }
      func2();

还是之前的函数,我们加入setTimeout后,当func2被压入调用栈后,setTimeout调用,也被压入调用栈中,它里面的回调函数(console.log(2))会进入消息队列,消息会在调用栈被清空时执行。随后setTimeout函数弹出调用栈,func1压入调用栈,执行完 console.log(1)后弹出调用栈;然后再执行console.log(3)随后func2弹出调用栈,调用栈被清空,消息队列中的消息会被压入到调用栈中,打印2 最后结果为1 3 2

使用promise,async,await创建的异步操作会加入到微任务队列中,它会在调用栈被清空的时候立即执行。并且处理期间新加进来的微任务也会一同执行。

示例三:

      var p = new Promise((resolve) => {
        console.log(4);
        resolve(5);
      });
      function func1() {
        console.log(1);
      }

      function func2() {
        setTimeout(() => {
          console.log(2);
        }, 0);
        func1();
        console.log(3);
        p.then((resolved) => {
          console.log(resolved);
        }).then(() => {
          console.log(6);
        });
      }
      func2();

在之前的示例代码中我们加入promise,执行顺序为:首先调用执行构造函数p,它被压入到调用栈中,执行里面的代码,打印出4,resolve(5),然后弹出。 后面的执行和之前一样,调用func2将console.log(2)放入消息队列,执行func1里的 console.log(1),执行console.log(3);之后promise.then的两个回调函数会入列到微任务队列中,后面func2弹出,调用栈清空后,把微任务队列中的 console.log(resolved);console.log(6)压入到调用栈中,打印出5和6,最后将消息队列中的消息压入调用栈中,打印出2,

最后执行结果为:4 1 3 5 6 2

宏任务 微任务

异步又分为宏任务和微任务,

宏任务(macro task)

  • 定时器;
  • 事件绑定;
  • ajax;
  • 回调函数;
  • Node中fs可以进行异步的I/O操作;

微任务(micro task)

  • Promise(async/await)  => Promise并不是完全的同步,在promise中是同步任务,执行resolve或者reject回调的时候,此时是异步操作,会先将then/catch等放到微任务队列。当主栈完成后,才会再去调用resolve/reject方法执行;
  • Process.nextTick (node中实现的api,把当前任务放到主栈最后执行,当主栈执行完,先执行nextTick,再到等待队列中找);
  • MutationObserver (创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用);

执行顺序优先级:SYNC => MICRO => MACRO。

微信图片_20220622154148.png

script脚本、setTimeout、setInterval这些都属于宏任务(macro) promise操作属于微任务

以上也是个人的一些浅薄的理解,如果有理解的不到位甚至错误的地方,还希望各位大佬不吝赐教~