资讯专栏INFORMATION COLUMN

js 执行机制 事件循环

JackJiang / 2119人阅读

摘要:事件完成,回调函数进入。我们来分析一段较复杂的代码,看看你是否真的掌握了的执行机制第一轮事件循环流程分析如下整体作为第一个宏任务进入主线程,遇到,输出。宏任务微任务第三轮事件循环宏任务执行结束,执行两个微任务和。

关于JavaScript

首先js是单线程的,执行任务肯定是一个接着一个。在最新的html5中提出了web-worker,但是JavaScript是单线程这一核心没有改变,一切js版本的多线程都是单线程模拟出来的,就像手机端的1像素也都是模拟出来的一样。

事件循环

如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

同步任务

异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。

当指定的事情完成时,Event Table会将这个函数移入Event Queue。

主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。

上述过程会不断重复,也就是常说的Event Loop(事件循环)。

我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

说了这么多文字,不如直接一段代码更直白:

let data = [];
$.ajax({    
url:www.javascript.com,    
data:data,    
success:() => {        
console.log("发送成功!");
    }
})console.log("代码执行结束");

上面是一段简易的ajax请求代码:

ajax进入Event Table,注册回调函数success。

执行console.log("代码执行结束")。

ajax事件完成,回调函数success进入Event Queue。

主线程从Event Queue读取回调函数success并执行。

相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。

又爱又恨的setTimeout

大名鼎鼎的setTimeout无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:

setTimeout(() => 
{    
console.log("延时3秒");
},3000)

渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?

先看一个例子:

setTimeout(() => {
    task();
},
3000)console.log("执行console");

根据前面我们的结论,setTimeout是异步的,应该先执行console.log这个同步任务,所以我们的结论是:

//执行console
//task()

去验证一下,结果正确!
然后我们修改一下前面的代码:

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

乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?

这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:

task()进入Event Table并注册,计时开始。

执行sleep函数,很慢,非常慢,计时仍在继续。

3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。

sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:

//代码1console.log
("先执行这里");
setTimeout(() => {    
console.log("执行啦")
},0);
//代码2console.log
("先执行这里");
setTimeout(() => 
{    
console.log("执行啦")
},3000);

代码1的输出结果是:

//先执行这里
//执行啦

代码2的输出结果是:

//先执行这里
// ... 3s later
// 执行啦

关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。

又恨又爱的setInterval

上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。

对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。

一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。

Promise与process.nextTick(callback)

传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。

Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:

macro-task(宏任务):包括整体代码script,setTimeout,setInterval

micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:

setTimeout(function()
 {    
 console.log("setTimeout");
})
 new Promise(function(resolve) 
 {    
 console.log("promise");
 resolve();
})
 .then(function() 
 {    
 console.log("then");
})
 console.log("console");

输出结果应该为:promise console then setTimeout
(原来promise对象pending状态并没有变为fulfiled,所以then里面的语句不会执行,感谢ElviraD的指出)
这段代码作为宏任务,进入主线程。

先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)

接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。

遇到console.log(),立即执行。

好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。

ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。

我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

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")
    })
})

第一轮事件循环流程分析如下:

整体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。那么第二轮时间循环从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。

第三轮事件循环开始,此时只剩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。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

写在最后

js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

setImmediate

微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。

最后的最后

javascript是一门单线程语言

Event Loop是javascript的执行机制

牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!

转载自公众号web前端

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90616.html

相关文章

  • JavaScript运行机制事件循环

    摘要:主线程不断重复上面的三步,此过程也就是常说的事件循环。所以主线程代码执行时间过长,会阻塞事件循环的执行。参考资料这一次,彻底弄懂执行机制任务队列的顺序机制事件循环搞懂异步事件轮询与中的事件循环 1. 说明 读过本文章后,您能知道: JavaScript代码在浏览器中的执行机制和事件循环 面试中经常遇到的代码输出顺序问题 首先通过一段代码来验证你是否了解代码输出顺序,如果你不知道输出...

    Ververica 评论0 收藏0
  • Js事件循环(Event Loop)机制以及实例讲解

    摘要:主线程要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。主线程循环即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。以上参考资料详解中的事件循环机制中的事件循环运行机制详解再谈 showImg(https://segmentfault.com/img/remote/1460000015317437?w=1920&h=1080); 前言 大家都...

    Anshiii 评论0 收藏0
  • 浅谈不同环境下的JavaScript执行机制 + 示例详解

    摘要:如果没有其他异步任务要处理比如到期的定时器,会一直停留在这个阶段,等待请求返回结果。执行的执行事件关闭请求的,例如事件循环的每一次循环都需要依次经过上述的阶段。因此,才会早于执行。 showImg(https://segmentfault.com/img/bVbnY76); 概念 同步任务(Synchronous) 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务 ...

    wanghui 评论0 收藏0
  • 【Node.js】理解事件循环机制

    摘要:前沿是基于引擎的运行环境具有事件驱动非阻塞等特点结合具有网络编程文件系统等服务端的功能用库进行异步事件处理线程的单线程含义实际上说的是执行同步代码的主线程一个程序的启动不止是分配了一个线程,而是我们只能在一个线程执行代码当出现资源调用连接等 前沿 Node.js 是基于V8引擎的javascript运行环境. Node.js具有事件驱动, 非阻塞I/O等特点. 结合Node API, ...

    Riddler 评论0 收藏0
  • JS浏览器事件循环机制

    摘要:事件循环机制事件循环机制分为浏览器和事件循环机制,两者的实现技术不一样,浏览器是中定义的规范,是由库实现。整个事件循环完成之后,会去检测微任务的任务队列中是否存在任务,存在就执行。 文章来自我的 github 博客,包括技术输出和学习笔记,欢迎star。 先来明白些概念性内容。 进程、线程 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。 线...

    zebrayoung 评论0 收藏0
  • JS JavaScript事件循环机制

    摘要:事件循环机制首先区分进程和线程进程是资源分配的最小单位系统会给它分配内存不同的进程之间是可以同学的,如管道命名管道消息队列一个进程里有单个或多个线程浏览器是多进程的,因为系统给它的进程分配了资源内存打开会有一个主进程,每打开一个页就有一个独 JS JavaScript事件循环机制 首先区分进程和线程 进程是cpu资源分配的最小单位(系统会给它分配内存) 不同的进程之间是可以同学的,如...

    dantezhao 评论0 收藏0

发表评论

0条评论

JackJiang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<