js代码执行代码过程

752 阅读5分钟

js代码执行过程通常分为以下三个阶段

  • 语法分析阶段

  • 预编译阶

  • 执行阶段

          当浏览器执行js脚本的时候,首先按顺序依次加载由script标签分割的js代码块,加载js代码块完毕后,立刻进入以上三个阶段,然后再按顺序查找下一个script标签包裹的代码块,再继续执行以上三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。

    语法分析阶段

          js脚本代码块加载完毕后,会首先进入语法分析阶段。该阶段主要是来分析js代码的语法是否有错误,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个script代码块;如果语法正确,则进入预编译阶段。

    预编译阶段

          js代码块通过语法分析阶段后,语法正确则进入预编译阶段。在分析预编译阶段之前,我们先了解一下js的运行环境,运行环境主要有三种:

    1. 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)

    2. 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)

    3. eval(不建议使用,会有安全,性能等问题)

    执行阶段

          js虽然是单线程执行的,但是在js代码执行过程中其实是有四个线程参与该过程,但是永远只有JS引擎线程在执行JS脚本程序,这就是所谓的js是单线程的原因,其他的三个线程只协助,不参与代码解析与执行。参与js执行过程的线程分别是:

  • JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)

  • 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件队列,等待JS引擎线程执行

  • 定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待JS引擎线程执行。 注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。

  • HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。 注:浏览器对通一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。

      永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。

       进入ES6或Node环境中,JS的任务分为两种,分别是宏任务(macro-task)和微任务(micro-task),在最新的ECMAScript中,微任务称为jobs,宏任务称为task。

在JS引擎执行过程中,进入执行阶段后,代码的执行顺序如下:

      宏任务(同步任务) -->检查是否有微任务(es6和node环境需要考虑 有的话就执行所有微任务) --> 宏任务(异步任务)

  • 宏任务(macro-task)同步任务
    console.log('script start');
    console.log('script end');
  • 微任务(micro-task) 主要有:Promise(es6), process.nextTick(node)
   Promise.resolve().then(function() {
       console.log('promise1');
   }).then(function() {
       console.log('promise2');
   });
  • 宏任务(macro-task)异步任务
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);

以上仅仅是js代码执行过程阶段的大概过程,在这里需要了解一个新的知识点,那就是事件循环(Event Loop)过程如下:

  • 首先执行宏任务的同步任务,在主线程上形成一个执行栈;

  • 当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制

  • 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中,等待主线程读取执行

  • 当JS引擎主线程上的任务执行完毕,检查是否存在可执行的微任务(ES6中或node环境),有的话执行所有微任务,然后读取任务队列的任务事件,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行

  • 当JS引擎主线程上的任务执行完毕后,再检查是否存在可执行的微任务(ES6中或node环境),有的话执行所有微任务,然后 则会再次读取任务队列中的事件任务,如此循环

   console.log('script start');
   setTimeout(function() {
       console.log('setTimeout');
   }, 0);
   new Promise(function(resolve){
       console.log('promise1');
       for(var i = 0; i <= 10000000; i++){
           if(i == 10000000)
           {
               resolve();
           }
       }
       console.log('promise2');
   }).then(function(){
       console.log('promise3')
   }).then(function() {
       console.log('promise4');
   });;

   console.log('script end');

执行结果如下

script start
promise1
promise2
script end
promise3
promise4
setTimeout
console.log('1');

setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
      console.log('3');
  })
  new Promise(function(resolve) {
      console.log('4');
      resolve();
  }).then(function() {
      console.log('5')
  })
})
process.nextTick(function() {
  console.log('6');
})
new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
      console.log('10');
  })
  new Promise(function(resolve) {
      console.log('11');
      resolve();
  }).then(function() {
      console.log('12')
  })
})

执行结果如下

1
7
6
8
2 
4
3
5
9
11
10
12

以上就是事件循环(Event Loop)的大概过程