JavaScript执行机制

原文

  简书原文:https://www.jianshu.com/p/0d2d42fbe1dc

大纲

  1、场景分析
  2、执行机制相关知识点
  3、以实例来说明JavaScript的执行机制
  4、相关概念

1、场景分析

/*
    以下这段代码的执行结果是什么?
    如果依照:js是按照语句出现的顺序执行这个理念,
    那么代码执行的结果应该是:
        //"定时器开始啦"
        //"马上执行for循环啦"
        //"执行then函数啦"
        //"代码执行结束"
    但结果并不是这样的,得到的结果是:
        //"马上执行for循环啦"
        //"代码执行结束"
        //"执行then函数啦"
        //"定时器开始啦"
*/
setTimeout(function(){
    console.log('定时器开始啦')
});

new Promise(function(resolve){
    console.log('马上执行for循环啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('执行then函数啦')
});

console.log('代码执行结束');

2、执行机制相关知识点

2.1、关于javascript

  javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的。

2.2、javascript的同步和异步

  单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
  如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
  JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
  于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

 

  1、同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  2、当Event Table中指定的事情完成时,会将这个函数移入Event Queue。
  3、主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  4、上述过程会不断重复,也就是常说的Event Loop(事件循环)。
  5、我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

 

 2.3、JavaScript的宏任务与微任务

   你是否觉得同步异步的执行机制流程就是JavaScript执行机制的全部?不是的,JavaScript除了广义上的的同步任务何异步任务,其对任务还有更精细的定义:
    macro-task(宏任务):包括整体代码script,setTimeout,setInterval
    micro-task(微任务):Promise,process.nextTick
  不同类型的任务会进入对应的Event Queue。
  事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

 

3、以实例来说明JavaScript的执行机制

 3.1、同步

console.log(1);
console.log(2);
console.log(3);
/*
    执行结果:1、2、3
    同步任务,按照顺序一步一步执行
*/

3.2、同步和异步

console.log(1);
setTimeout(function() {
    console.log(2);
},1000)
console.log(3);
/*
    执行结果:1、3、2
    同步任务,按照顺序一步一步执行
    异步任务,放入消息队列中,等待同步任务执行结束,读取消息队列执行
*/

3.3、异步任务进一步分析

console.log(1);
setTimeout(function() {
    console.log(2);
},1000)
setTimeout(function() {
    console.log(3);
},0)
console.log(4);
/*
    猜测是:1、4、2、3   但实际上是:1、4、3、2
    分析:
        同步任务,按照顺序一步一步执行
        异步任务,当读取到异步任务的时候,将异步任务放置到Event table(事件表格)
中,当满足某种条件或者说指定事情完成了(这里的是时间分别是达到了0ms和1000ms)当指定
事件完成了才从Event table中注册到Event Queue(事件队列),当同步事件完成了,便从
Event Queue中读取事件执行。(因为3的事情先完成了,所以先从Event table中注册到
Event Queue中,所以先执行的是3而不是在前面的2)
*/

3.4、宏任务和微任务

console.log(1);
setTimeout(function() {
    console.log(2)
},1000);

new Promise(function(resolve) {
    console.log(3);
    resolve();
}
).then(function() {
    console.log(4)
});
console.log(5);
/*
    以同步异步的方式来判断的结果应该是:1、3、5、2、4
    但是事实上结果是:1、3、5、4、2
    为什么是这样呢?因为以同步异步的方式来解释执行机制是不准确的,更加准确的方式是宏任务和微任务:
    因此执行机制便为:执行宏任务 ===> 执行微任务 ===> 执行另一个宏任务 ===> 不断循环
        即:在一个事件循环中,执行第一个宏任务,宏任务执行结束,执行当前事件循环中的微任务,
执行完毕之后进入下一个事件循环中,或者说执行下一个宏任务
*/

3.5、是否彻底理解JavaScript执行机制实例

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、 第一轮事件循环流程分析如下:
    整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
    遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
    遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
    遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
    又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
        
    宏任务Event Queue   微任务Event Queue
    setTimeout1         process1
    setTimeout2         then1
    
    上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
    我们发现了process1和then1两个微任务。
    执行process1,输出6。
    执行then1,输出8。
    
    好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。
    
2、 那么第二轮时间循环从setTimeout1宏任务开始:
    
    首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,
记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
    
    宏任务Event Queue     微任务Event Queue
    setTimeout2           process2
                          then2
                          
    第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
        输出3。
        输出5。
        第二轮事件循环结束,第二轮输出2,4,3,5。

3、 第三轮事件循环开始,此时只剩setTimeout2了,执行。
        直接输出9。
        将process.nextTick()分发到微任务Event Queue中。记为process3。
        直接执行new Promise,输出11。
        将then分发到微任务Event Queue中,记为then3。
        
    宏任务Event Queue     微任务Event Queue
                            process3
                            then3      
    第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
        输出10。
        输出12。
        第三轮事件循环结束,第三轮输出9,11,10,12。

    整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
*/

4、相关概念

4.1、JS为什么是单线程的?

   JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
  JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
  所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
  为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

 4.2、JS为什么需要异步?

   如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。

 4.3、JS单线程又是如何实现异步的呢?

   既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢?
  是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制。

 4.4、任务队列

   "任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
  "任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
  所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  "任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
  读取到一个异步任务,首先是将异步任务放进事件表格(Event table)中,当放进事件表格中的异步任务完成某种事情或者说达成某些条件(如setTimeout事件到了,鼠标点击了,数据文件获取到了)之后,才将这些异步任务推入事件队列(Event Queue)中,这时候的异步任务才是执行栈中空闲的时候才能读取到的异步任务。

 4.5、Event Loop

  主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
  Event Loop是javascript的执行机制

 4.6、setTimeout(fn,0)

   setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。
  HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
  需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

参考网址

https://juejin.im/post/59e85eebf265da430d571f89
https://www.cnblogs.com/MasterYao/p/5563725.html
https://blog.csdn.net/highboys/article/details/79110116

 

posted @ 2018-07-17 20:46  前端路上的小兵  阅读(...)  评论(...编辑  收藏