huandie2012 / blog

Introduction of knowledge points
3 stars 1 forks source link

JS执行机制 #35

Open huandie2012 opened 5 years ago

huandie2012 commented 5 years ago

JS 是单线程语⾔,一切javascript版的"多线程"都是用单线程模拟出来的。 JS是按照语句出现的顺序执⾏行的

let a = '1'
console.log(a)
let b = '2'
console.log(b)
//1  2

然⽽,实际上代码执⾏的顺序并不是按照出现的顺序执⾏行:

setTimeout(function(){
    console.log('one')
});
console.log('two')
// two one

这是为什什么呢? JS为什什么是单线程的 原因在于:JS的用途。JS是浏览器脚本语⾔,其主要的作⽤是与⽤户交互、操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,⼀个线程在某个DOM节点上添加内容,另一个线程删除了了这个节点,这时浏览器器应该以哪个线程为准? 为了利⽤多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是⼦线程完全受主线程控制,且不不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。 Web Worker:主线程可以采⽤用new命令,调⽤用Worker()构造函数,新建⼀一个 Worker线程。具体可参考:Web-Worker 同步任务和异步任务 由于JS是单线程,所以任务执⾏都是一个⼀个执⾏,前⼀个执⾏完了之后,才能执⾏下⼀个任务,如果前⼀个任务阻塞了,后⾯面的任务只能等待。 如此,将会造成资源的浪费。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输⼊入输出设备)很慢(⽐比如Ajax操作从⽹网络读取数据),不得不等着结果出来,再往下执行。 JavaScript语⾔的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运⾏排在后⾯的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。 因此,JS设计者将所有任务分为:

JS执行的运行机制如下: image

  1. 同步和异步任务分别进⼊不同的执行"场所",所有同步任务都在主线程上执行,形成⼀个执行栈。
  2. 主线程之外,还存在⼀个"任务队列"(Event Queue),当指定的事情完成时,即只要异步任务有了运行结果,Event Table会将这个函数移入Event Queue。
  3. 主线程内的任务执⾏行行完毕为空,会去Event Queue读取对应的函数,进⼊入主线程执⾏行行栈,并开始执⾏行行。
  4. 上述过程会不不断重复,也就是常说的Event Loop(事件循环)。 如何判断主线程执⾏行行栈为空? js引擎存在monitoring process进程,会持续不不断的检查主线程执⾏行行栈是否为空,⼀一旦为空,就会去Event Queue那⾥里里检查是否有等待被调⽤用的函数。 任务队列 “任务队列列”是⼀一个先进先出的数据结构,排在前⾯面的事件,优先被主线程读取。主线程的读取过程基本上是⾃自动的,只要执⾏行行栈⼀一清空,”任务队列列”上第⼀一位的事件就⾃自动进⼊入主线程。但是,由于存在后⽂文提到的”定时器器”功能,主线程⾸首先要检查⼀一下执⾏行行时间,某些事件只有到了了规定的时间,才能返回主线程。 JS的事件循环机制eventLoop 我们先来看⼀一个异步请求的代码:
    let data = [];
    $.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
      console.log('发送成功!');    
    }})
    console.log('代码执⾏行行结束');

    ⽤用JS的执⾏行行机制分析:

  5. ajax是异步任务,进⼊入Event Table,注册回调函数success,进⾏行行任务挂载。
  6. 执⾏行行同步任务console.log('代码执⾏行行结束'),直到主线程执⾏行行完毕。
  7. ajax事件完成,回调函数success进⼊入Event Queue。(异步事件的执⾏行行,可以和同步事件同时进⾏行行,事件完成之后,回调进⼊入任务队列列,等待主线程执⾏行行完毕后在执⾏行行任务队列列中的事件)
  8. 主线程从Event Queue读取回调函数success并执⾏行行。 通过以上,我们了了解了了JS的eventLoop,我们就可以轻松回答前⾯面提出的问题:
    setTimeout(function(){
    console.log('one')
    });
    console.log('two')
    // two one

    为什什么这段代码不不是按照顺序执⾏行行的? setTimeout和setInterval

    setTimeout(() => {    
    task()
    },3000)
    sleep(10000000)

    为什什么rask()等待执⾏行行的事件超过3秒?执⾏行行过程分析:

  9. task()进⼊入Event Table并注册,计时开始。
  10. 执⾏行行sleep函数,很慢,⾮非常慢,计时仍在继续。
  11. 3秒到了了,计时事件timeout完成,task()进⼊入Event Queue,但是sleep也太慢了了吧,还没执⾏行行完,只好等着。
  12. sleep终于执⾏行行完了了,task()终于从Event Queue进⼊入了了主线程执⾏行行。 setTimeout这个函数,经过指定时间后,把要执⾏行行的任务(本例例中为task())加⼊入到Event Queue中,⼜又因为是单线程任务要⼀一个⼀一个执⾏行行,如果前⾯面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远⼤大于3秒。
    console.log('先执⾏行行这⾥里里');
    setTimeout(() => {
    console.log('执⾏行行啦')
    },0);

    关于setTimeout要补充的是,即便便主线程为空,0毫秒实际上也是达不不到的。根据HTML的标准,最低是4毫秒。 setInterval会每隔指定的时间将注册的函数置⼊入Event Queue,如果前⾯面的任务耗时太久,那么同样需要等待。 对于setInterval(fn,ms)来说,我们已经知道不不是每过ms秒会执⾏行行一次fn,⽽而是每过ms秒,会有fn进⼊入Event Queue。一旦setInterval的回调函数fn执⾏行行时间超过了了延迟时间ms,那么就完全看不不出来有时间间隔了了。 宏任务和微任务 除了了⼴广义的同步任务和异步任务,任务可以更更精细的定义为:

    • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
    • micro-task(微任务):Promise,process.nextTick 不同类型的任务会进⼊入对应的Event Queue,process.nextTick(callback)类似node.js版的"setTimeout"

事件循环的顺序,决定js代码的执⾏行行顺序。进⼊入整体代码(宏任务)后,开始第⼀一次循环。接着执⾏行行所有的微任务。然后再次从宏任务开始,找到其中⼀一个任务队列列执⾏行行完毕,再执⾏行行所有的微任务。例如:

setTimeout(function() {
   console.log('setTimeout');
})
new Promise(function(resolve) {
   console.log('promise');    
   resolve('ok')
}).then(function() {
   console.log('then');
})
console.log('console');
  1. 这段代码作为宏任务,进⼊入主线程。
  2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下⽂文不不再描述)
  3. 接下来遇到了了Promise,new Promise⽴立即执⾏行行,then函数分发到微任务EventQueue。
  4. 遇到console.log(),⽴立即执⾏行行。
  5. 好啦,整体代码script作为第⼀一个宏任务执⾏行行结束,看看有哪些微任务?我们发现了了then在微任务Event Queue⾥里里⾯面,执⾏行行。
  6. ok,第⼀一轮事件循环结束了了,我们开始第⼆二轮循环,当然要从宏任务EventQueue开始。我们发现了了宏任务Event Queue中setTimeout对应的回调函数,⽴立即执⾏行行。
  7. 结束。
    // 输出结果
    promise
    console
    then
    setTimeout

    事件循环,宏任务,微任务的关系如图所示: image 最后,我们来看个例子:

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

    有兴趣的小伙伴可以执行一下代码看最后的结果。 (结果是:156234789) 事件执行顺序的题目 总结: 关于js是单线程语言的理解:js本身是单线程,但执行js的环境(浏览器,node)是多线程的