js的执行机制

js在哪执行

js的执行引擎基于v8(c++编写),在chrome和node中都有应用,执行时有如下两部分构成javascript

  • 内存堆(内存分配)
  • 调用栈(代码执行)

上述两部分的联系就是代码在调用栈中执行,执行过程当中会存取一些对象在内存堆上。java

咱们写的js代码通过js引擎(解释器)转化为高效的机器码,如今的v8引擎由TurboFan和Ignition两部分构成,其中Ignition是解释器,而TurboFan主要对代码作些优化,以提升执行性能。node

基于执行引擎的执行原理在代码层面咱们能够作些优化,能够参考我以前的一篇文章c++

js如何执行

js同步执行

js按照代码顺序执行,在栈上分配执行空间,按照调用顺序,会有出栈入栈等各类状况,比较好分析,惟一值的说的地方就是js只有一个主线程,栈空间有限,若是递归执行过深会发生溢出,因此在编写代码层面须要注意这种状况。chrome

js异步执行

为何要有异步?

同步单线程代码处理起来方便,代码表达也容易,更符合咱们的思惟方式,为何还会出现异步呢?
由于同步会发生阻塞,在如今这个高并发时代,不能很好的处理海量请求,同时也不能充分利用硬件资源(想一想cpu和io之间处理速度差别你就深有体会)。
可是为何很少线程呢,例如java,主要是单个线程上运行代码相对来多线程来讲说容易写,没必要考虑在多线程环境中出现的复杂场景,例如死锁等等。segmentfault

异步执行机制?

异步执行相对来讲复杂些,因此详细描述下,关键是在各类使用状况下执行顺序问题,在此就须要引入一个概念-->Event Loop。结合下面这幅图进行大体说明下:
event-loop-gcyapi

Event Loop的概念
  • 全部任务在主线程上执行,造成一个执行栈(execution context stack),上图stack区域所示。
  • 执行过程当中可能会调用异步api,其中Background Threads负责具体异步任务执行,结束后将宏任务回调逻辑放入task queue中,微任务回调逻辑放入micro task队列中。
  • 主线程执行完毕,检查microtask队列是否为空,会执行到队列空为止
  • 从宏任务队列中取出一个在执行,执行完后,检查并取出执行microtask队列的任务,而后不断重复这个步骤,对于这整个循环过程,一个对应的描述名词就叫作event loop。

node中异步

异步任务分类promise

  • macrotask类型包括 script总体代码,setTimeout,setInterval,setImmediate,I/O……
  • microtask类型包括 Promise process.nextTick Object.observe MutaionObserver……

node中event loop各个阶段的操做以下图所示session

node-event-loop
说明,上图中每一个盒子表示了event loop的一个阶段,每一个阶段执行完毕后,或者执行的回调数量达到上限后,event loop会进入下个阶段。多线程

timers: 在达到这个下限时间后执行setTimeout()和setInterval()这些定时器设定的回调。
I/O callbacks: 执行除了close回调,timer的回调,和setImmediate()的回调,例如操做系统回调tcp错误。
idle, prepare: 仅内部使用。
poll: 获取新的I/O事件,例如socket的读写事件;node会在适当条件下阻塞在这里,若是poll阶段空闲,才会进入下一阶段。
check: 执行setImmediate()设定的回调。
close callbacks: 执行好比socket.on('close', ...)的回调。

下面结合一些具体例子进行说明

require('fs').readFile('./case1.js', () => {
    setTimeout(() => {
        console.log('setTimeout in poll phase');
    });
    setImmediate(() => {
        console.log('setImmediate in poll phase');
    });
});
输出结果是:
setImmediate in poll phase
setTimeout in poll phase
Process finished with exit code 0

说明 setImmediate的回调永远先执行,由于readFile的回调执行是在 poll 阶段,因此接下来的 check 阶段会先执行 setImmediate 的回调。

setTimeout(() => console.log('setTimeout1'), 1000);
setTimeout(() => {
    console.log('setTimeout2');
    process.nextTick(() => console.log('nextTick1'));
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);

process.nextTick(() => console.log('nextTick2'));
process.nextTick(() => {
    process.nextTick(console.log.bind(console, 'nextTick3'));
});
 Promise.resolve('xxx').then(() => {
    console.log('promise');
    testPromise();
});
process.nextTick(() => console.log('nextTick4'));

结果是:

nextTick2
nextTick4
nextTick3
promise
setTimeout2
setTimeout3
nextTick1
setTimeout1

在描述什么是event loop中,大概描述了microtask机制,但具体到nextTick比较特别,有一个Tick-Task-Queue专门用于存放process.nextTick的任务,且有调用深度限制,上限是1000。js引擎执行 Macro Task 任务结束后,会先遍历执行Tick-Task-Queue的全部任务,紧接着再遍历 Micro Task Queue 的全部任务。具体执行逻辑能够下面代码表示。

for (macroTask of macroTaskQueue) {

    // 1. Handle current MACRO-TASK
    handleMacroTask();

    // 2. Handle all NEXT-TICK
    for (nextTick of nextTickQueue) {
        handleNextTick(nextTick);
    }

    // 3. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

因此才会先输出process.nextTick而后才会是promise,其它的输出顺序不在赘述,前面讲event-loop机制时已经说明了。根据上面代码表述的执行逻辑,很显然能够获得下面的个结论,当递归调用时会发生死循环,而宏任务就不会。

testPromise();
function testPromise() {
    promise = Promise.resolve('xxx').then(() => {
        console.log('promise');
        testPromise();
    });
}
//将以前步骤的promise任务换成这个,setTimeout2以及以后的输出永远没机会出来,类比到nextTick也是这种效果

看了一些书,参考了不少资料,将本身学习的东西,理解后在输出,但愿你们辩证的看待,有空的话接下来研究一下源码,毕竟经过demo验证结论的说服力没有源码来的那么直接。

参考连接
https://jakearchibald.com/201...
https://blog.sessionstack.com...
https://cnodejs.org/topic/592...
https://developer.mozilla.org...
https://nodejs.org/en/docs/gu...

相关文章
相关标签/搜索