JavaScript是如何工作的

2018-12-09  本文已影响0人  安静的牛蛙

作为一个记录,记录下对JS内部执行机制的总结

一:V8引擎

Google的V8引擎是最流行的一个JS运行环境,通过研究V8引擎来了解JS内部执行机制。

二:V8内部架构

1:JS进程环境


V8引擎

这个图代表了JS解释器运行的环境,其中包括了

2:Web API
单独的JS运行环境只能处理一些纯逻辑的内容。如果想完成完整的工作,还需要一些外部接口的协助。这些被成为Web Api。常见的有负责DOM的Document,负责Ajax的XMLHttpRequest,以及负责外部定时的window.setTimeout等。


外部API

其中除了API外,还有两个最常用的两个机制

三:详细解析

JS解释器内部

1:内存堆
内存堆用于存放所有在JS运行过程中创建的全局变量,对象。内存堆中资源的分配和回收管理依赖于JS的GC机制,也就是垃圾回收机制。具体的可以参考这篇文章。GC使用的回收算法称为标记算法。标记清除算法有3个步骤:

解释器外部的Event Loop & CallbackQueue

在JS的主线程里面,所有代码的执行都是单线程,同步执行的,通过调用栈的机制实现。但我们在实际使用总还需要一些异步操作,比如定时执行,比如ajax,以及新增的Promise等。这些异步操作的执行,则是依赖Event Loop & CallbackQueue来实现的。
首先关于EventLoop 和 CallbackQueue有以下几点需要知道:

在了解上面的内容后,我们继续看下JS究竟是按照什么样的顺序执行代码的。首先给出结论,JS按照下面的次序进行代码的执行。

  1. 执行完主执行线程中的任务。
  2. 取出Microtask Queue中任务执行直到清空
  3. 取出Macrotask Queue中一个任务执行。
  4. 取出Microtask Queue中任务执行直到清空
  5. 重复3和4。。。。

如果以伪代码进行表示的话,就是

while (true) {
  宏任务队列.shift()  // 推出一个宏任务,进行调用
  微任务队列全部任务()  // 执行当前所有的微任务
}

下面我们先看个例子:

console.log('start')
setTimeout( function () {
  console.log('setTimeout0')
  new Promise(function(resolve) {
    console.log('promise3')
    resolve()
  }).then(function() {
    console.log('promise4');
  })
}, 0)

setTimeout( function () {
  console.log('setTimeout1')
}, 0)

new Promise(function(resolve) {
  console.log('promise0')
  resolve()
}).then(function() {
  console.log('promise1');
}).catch(function() {
  console.log('promise2');
});

console.log('end')

运行的结果可以点击demo进行查看。下面逐条分析:

// 执行主线程 
// console.log('start')
=> start  // 首先先执行主线程里面的任务。因此第一条被打印的是start
// setTimeout( function () {
//   console.log('setTimeout0')
//   new Promise(function(resolve) {
//     console.log('promise3')
//     resolve()
//   }).then(function() {
//     console.log('promise4');
//   })
// }, 0)
== 碰到了setTimeout,将回调函数cb1交给timer,timer会在定时时间到的时候,将cb1添加到宏任务队列
// setTimeout( function () {
//   console.log('setTimeout1')
// }, 0)
== 碰到了setTimeout,将回调函数cb2交给timer,timer会在定时时间到的时候,将cb2添加到宏任务队列,因为两个定时器的定时时间相同,但cb2在cb1之后调用,因此,添加到宏定义队列的时候,先添加cb1,再添加cb2上面的
// new Promise(function(resolve) {
//   console.log('promise0')
=> promise0 // 执行新建Promise代码,打印promise0,这是第二条打印
//   resolve()
== 当Promise碰到resolve函数时,将其对应的then代码中的回调cb3,放到微任务队列中
// }).then(function() {
//   console.log('promise1');
// }).catch(function() {
//   console.log('promise2');
// });

// console.log('end')
=> end // 新建Promise执行完后,继续执行,打印end
== 至此,我们完成了第1阶段的主执行线程中的任务,然后执行第2阶段,查询微任务队列,发现了cb3函数。将其取出,然后进行执行。
// console.log('promise1');   // cb3的代码,执行后打印
=> promise1
== 所有的微任务已经执行完毕,进入第3阶段然后执行宏任务队列中的第一个任务,也就是cb1
//   console.log('setTimeout0')
=> setTimeout0 
//   new Promise(function(resolve) {
//     console.log('promise3')
=> promise3
//     resolve()  
== 当Promise碰到resolve函数时,将其对应的then代码中的回调cb4,放到微任务队列中
//   }).then(function() {
//     console.log('promise4');
//   })
== 执行完cb1后,执行第4阶段,重新查询微任务队列,发现了cb4,将其执行
//     console.log('promise4');  // cb4
=> promise4
== 微任务队列执行完毕后,重复第3阶段,查询宏任务队列,会发现,还有一个cb2没有执行,取出后执行
// console.log('setTimeout1') // cb2的代码,执行后打印
=> setTimeout1

######## 最后总结后的输出如下 ########
start 
promise0
end
promise1
setTimeout0 
promise3
promise4
setTimeout1

OK,我们已经理解了当一段代码中,存在同步代码,以及各种各样的异步代码的时候的Event Loop是如何调度代码执行的。那么如果存在多段代码的情况呢?比如说存在多个js文件,此时的代码是如何调度执行的呢?

在继续下面的内容前,需要声明一点:

在浏览器加载页面过程中,可以认为初始执行线程中没有代码,每一个script标签中的代码都是宏任务中的script(整体代码),是一个独立的task。即会执行完前面的script中创建的microtask再执行后面的script标签中的代码。

为了加深理解,我们以实际浏览器加载一个网页为例:

  1. 首先浏览器解析HTML文件,获取到其中包括的JS代码。这些代码由一个个的script标签包含的代码块组成(无论代码是在HTML文件中,还是在单独的JS文件中)。
  2. 首先所有的代码块都作为script(整体代码),成为一个个宏任务的task,按照顺序放到了宏任务队列中去。
  3. 开始从宏任务队列中的取出一个代码块,放入到主线程中,进行执行
  4. 主线程执行代码块过程中,当碰到异步操作的时候,根据异步操作的类型,放置到不同的任务队列中去。
  5. 当主线程执行代码完毕后,执行当前微任务队列中的所有任务。
  6. 微任务执行完毕后,继续从宏任务队列中取下一个任务
  7. 重复 3,4,5,6。。。

下面,拿一个实际例子进行分析,为了方便,我们不演示存在多个JS文件的情况,只是使用了多个script标签,分割成多个代码块。这样的情况和存在JS文件是一样的,可以自己实际操作体验下。可以点击查看demo(JSBin会将所有的script打包成一个进行处理,codepen则不会)的具体运行结果。其代码如下

<body>
  <script>
    setTimeout(function () {
      console.log('timeout1');
    })

    new Promise(function (resolve) {
      console.log('promise1');
      for (var i = 0; i < 1000; i++) {
        i == 99 && resolve();
      }
      console.log('promise2');
    }).then(function () {
      console.log('then1');
    })

    console.log('global1');
  </script>
  <script>
    console.log('global2');

    new Promise(function (resolve) {
        console.log('promise3');
        for (var i = 0; i < 1000; i++) {
            i == 99 && resolve();
        }
        console.log('promise4');
    }).then(function () {
        console.log('then2');
    })

    setTimeout(function () {
        console.log('timeout2');
    })
  </script>
  <script>
    console.log('global3');
  </script>
</body>

下面具体分析流程:

1:首先,浏览器解析html代码,发现存在三个script代码块,将其划分为三个宏任务,依次添加到宏任务队列中去
2:执行主线程,此时为空。搜索微任务队列,发现也为空,则取宏任务队列的第一个任务。
3:第一个任务为第一个代码块,将其放入到主线程中进行执行
     // 首先 ,碰到了setTimeout函数,将cb1(console.log('timeout1');)放入到宏任务队列中去
     // 碰到了新建Promise代码,输出打印,然后,将then中的cb2(console.log('then1');)放入到微任务队列中去
     => promise1
     => promise2
    // 最后执行打印global1的代码
     => global1
4:执行完毕第一个代码块后,查询微任务队列,发现了cb2回调,将其取出执行
     => then1
5:执行完微任务队列后,重新查询宏任务队列,将第二个代码块取出,调度到主线程中执行
     // 首先 打印global2
     => global2
     // 碰到新建Promise代码,输出打印,然后,将then中的cb3(console.log('then2');)放入到微任务队列中去
     => promise3
     => promise4
     // 碰到了setTimeout命令,将回到cb4(console.log('timeout2');)放入到宏任务队列中去后,执行完毕
6:执行完毕第一个代码块后,查询微任务队列,发现了cb3回调,将其取出执行
     => then2
7:执行完微任务队列后,重新查询宏任务队列,将第三个代码块取出,调度到主线程中执行
     => global3
8:执行完第三个代码块后,查询微任务队列发现为空,则继续查询宏任务队列,取出执行第一个代码块时放入的cb1函数,执行
     => timeout1
9:执行完后,查询微任务队列,发现依然为空,则继续查询宏任务队列,发现执行第二个代码时放入的cb4函数,执行
     => timeout2
###### 最终输入 ######
promise1
promise2
global1
then1
global2
promise3
promise4
then2
global3
timeout1
timeout2

OK,如果确定已经搞明白后,可以尝试,将上面的代码块重新组织,比如讲第一个script和第二个script中的代码放到同一个script中,分析下输出会发生什么变化。然后执行下,验证下自己的结果是否正确。

在了解了各类异步操作的调用规律后,我们再看看实际使用中的会面临的几个问题,

四:总结

在上面的描述中,我们将JS在它的运行环境是如何运行的进行了详细介绍。包括主线程的运行规则,其中涉及到执行上下文,闭包,scope-chain,语法环境等,以及主线程运行中内存的使用,如何避免出现内存泄漏。另外还详细归纳了浏览器如何通过Event Loop调度各类代码进入到主线程执行的规律,其中涉及到了宏任务队列和微任务队列,以及由此引发的实际使用中需要注意的事项。后面,会寻找其中的具体各项,进行展开,加强自己的记忆。

参考链接

https://blog.csdn.net/mogoweb/article/category/2654805
https://zhuanlan.zhihu.com/p/26229293
https://zhuanlan.zhihu.com/p/26238030
https://juejin.im/post/5b3847aee51d4558bd51a6dd
https://juejin.im/post/5aa5dcabf265da239c7afe1e
http://imweb.io/topic/5a27610da192c3b460fce29f
http://zhouweicsu.github.io/blog/2018/05/05/javascript-event-loop/
http://davidshariff.com/blog/javascript-scope-chain-and-closures/

上一篇 下一篇

猜你喜欢

热点阅读