js是怎么运行的?

955 阅读3分钟

js是如何运行的?

依靠js引擎

js引擎负责检查js代码语法,使用编译器将js代码转成机器码,供电脑执行。 js引擎由两部分组成:

  • 堆(Memory Heap), JS引擎中给对象分配的内存空间是放在堆中的。如var foo = {name: 'foo'} 那么这个foo所指向的对象是存储在堆中的。
  • 栈(Call Stack), 用来存储方法调用的地方,以及基础数据类型(如var a = 1)也是存储在栈里面的,会随着方法调用结束而自动销毁掉(入栈-->方法调用后-->出栈)。

栈(Call Stack)

JS被设计为单线程运行的,对于Call Stack中的每个方法调用,都会形成它自己的一个执行上下文Execution Context(关于执行上下文详细阐述请访问这里). JS引擎的线程与浏览器的渲染线程是互斥的关系,在JS线程运行时候,渲染线程是暂停的。因此JS如果有阻塞产生会导致浏览器卡死。

依靠RunTime

RunTime是浏览器提供的Web API,事件循环(Event Loop)和事件队列(Callback Queue),这些可以在运行时供JS调用。

事件循环(Event Loop)

Event Loop只做一件事情,负责监听Call Stack和Callback Queue。当Call Stack里面的调用栈运行完变成空了,Event Loop就把Callback Queue里面的第一条事件(其实就是回调函数)放到调用栈中并执行它,后续不断循环执行这个操作: 。 详细请参考How JavaScript works

总结

  • JS引擎主要负责把JS代码转为机器能执行的机器码,而JS代码中调用的一些WEB API则由其运行环境提供,这里指的是浏览器。

  • JS是单线程运行,每次都从调用栈出取出代码进行调用。如果当前代码非常耗时,则会阻塞当前线程导致浏览器卡顿。

  • 回调函数是通过加入到事件队列中,等待Event Loop拿出并放到调用栈中进行调用。只有Event Loop监听到调用栈为空时,才会从事件队列中从队头拿出回调函数放进调用栈里。

参考文章: JavaScript 运行原理解析

ES6 提出了Job Queue

es6提出了JOB Queue的概念,是为了promise。

job queue的实现就是微任务队列,所以一个callbackQueue 里面分为宏任务队列和微任务队列。

微任务队列比宏任务队列优先级高,当调用栈是空的时候,会优先看微任务列表里有没有任务,有的话,会全部调用完,再判断宏任务队列。看下面代码:

var promise = new Promise(function (resolve, reject) { resolve(1) });
promise.then(function (resolve) { console.log(1) }); // 放微任务里
console.log('a'); // 调用栈直接执行
promise.then(function (resolve) {
    console.log(2);
    promise.then(function () {
        console.log('charu')
    })
});// 放微任务里
setTimeout(function () {
    console.log('h1');
    promise.then(function(){
        console.log('charu2')
    })
}, 0);// 放宏任务里
setTimeout(function () {
    console.log('h2');
    promise.then(function(){
        console.log('charu3')
    })
}, 0);// 放宏任务里
promise.then(function (resolve) { console.log(3) });// 放微任务里
console.log('b'); // 调用栈直接执行

执行结果:

a
b
1
2
3
charu // 先把所有微任务执行完
h1 // 再执行宏任务
charu2 // 执行完一个宏任务,就回去检查下有没有微任务,有的话,就一次性执行完
h2 // 执行完微任务,再执行宏任务
charu3

宏任务有:setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering和鼠标事件,键盘事件,网络事件和html解析

微任务有:process.nextTick, Promises, MutationObserver,和 dom mutations操作;

宏任务和微任务

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们成为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。