js for循环_[javascript基础]事件循环机制(调用栈与事件队列)

c6254f0ed04f5a69b82a280b5b6c86c3.png
其实这一块的理解特别重要, 前端的同学们千万要重视

为什么我要说事件循环机制特别重要呢?

举个栗子,相信很多同学面试的都看过这道题:

for(var i = 0; i< 10 ; i++) {
   setTimeout(function() {
      console.log(i);
   }, 0);
}

相信很多同学都知道答案,代码执行完之后就会出现10个10,但是如果同学们面的是中高级前端岗位,面试官基本上还会问一句: 为什么会是10个10?

这个很多同学,都只会回答异步的问题.

虽然,其实这种回答方式虽然没有什么错误,但是也肯定不是面试官想要听到的回答,因为作为中高级的前端岗位,对js内置的事件循环机制是必须掌握的.

对于部分大厂,这个部分也是实习生必须掌握的一个知识点

而且,解释完这个,你还需要解释下面这段代码.

如果不了解事件循环机制,只能呜噜呜噜,说几句块级作用域云云的就混过去了.

你的水平在面试官看来一下子就低了很多很多

for(let i = 0; i< 10 ; i++) {
   setTimeout(function() {
      console.log(i);
   }, 0);  // 答案是 0,1,2,3,4,5,6,7,8,9
}

以上的所有内容只是为了让同学们知道,js事件循环机制的重要性

下面开始介绍正题:


js事件循环机制

首先先聊一下js这种语言,很多人说js是一种单线程语言,其实这不是特别准确.

准确的说法应该是js是一种单个主线程的浏览器语言.

而除了主线程,还包括诸如h5提供的web-worker多线程(不懂的同学可以看一下我之前的文章),还有就是今天我们主要要学习的调用栈(call stack)


  1. 调用栈(call stack)

说起栈,相信大家都不会觉得陌生,在学习七大基本数据类型的时候.

我们都有了解过,栈(stack)负责存贮简单数据本身和复杂数据类型的地址指针,堆(heap)则负责存储复杂数据类型数据本身.

栈有一个特点: 入口和出口只有一个, 我们可以将其想象成一个水桶.

入栈也称压栈,出栈也称弹栈.

而我们今天要介绍的调用栈(call stack)也具有同样的特征.

举个例子

function a() {  
  console.log("I'm a!");
};

function b() {  
  a();
  console.log("I'm b!");
};

b();

假设我们这个js文件名字叫做main.js

此时call stack里面的情况就是

函数a;

函数b;

main.js;

换句话说,main.js位于栈底,上面压着函数b,函数b上面又压着函数a;

随后,函数a执行完,打印了"I'm a!"后,出栈.

紧接着,函数b执行完,打印了"I'm b!"后,出栈.

最后,整个mian,js执行完,main.js出栈.

整个main.js就在这样的调用栈机制下完成了运行.

但是!!! 如果我们有异步任务(async task)的时候,这种栈机制就很那满足我们的需求了

我们要怎么办呢?

没关系,因为js还为我们提供了一个任务队列(task queue)机制


2. 任务队列(task queue)

拿我们最常见的异步任务setTimeout来举例.

当我们的js文件从上往下解析的时候,碰到类似setTimeout这种异步任务的时候并不会直接执行,而是将其暂时挂起在webcore模块上.

异步任务在webcore中执行,完成后会通知调用栈(call stack),并将其同步部分(一般是回调)放入任务队列(task queue)里面去.

而主线程的任务完成后,就会去执行任务队列(task queue)里面的内容.

而整个过程叫做 事件循环

上张图来理解一下:

17f16a1f792eaecc16463a8383518747.png

这个时候,我们再回头看一下之前的那个问题,你知道要怎么回答了吗?

for(var i = 0; i< 10 ; i++) {
   setTimeout(function() {
      console.log(i);
   }, 0);
}

没错,你该这么说:

首先,script部分入栈,for循环作为同步任务,率先执行,而setTimeout作为异步任务被webcore中的timer模块暂时挂起,等到主进程执行完,由于var出来的变量位于他自身的函数作用域当中,是会被for循环重复赋值.

换句话说,主线程执行完后i已经变成了10,此时才会开始执行任务队列中的console.log部分,这个时候当然会打印出10个10;

而这个问题,你又需要怎么回答呢?

for(let i = 0; i< 10 ; i++) {
   setTimeout(function() {
      console.log(i);
   }, 0);  // 答案是 0,1,2,3,4,5,6,7,8,9
}

首先,你需要回答出es6提供的let声明方式,存储的变量会单独开辟出一个作用域,被我们称之为"块级作用域";

每个块级作用域依次入栈出栈(进去了执行完,立即出,不会出现堆栈的情况),每次执行完都会执行一次任务队列,此时的i还保持着for循环当时的赋值,所以答案变成了0,1,2,3,4,5,6,7,8,9

相信各位同学应该已经明白了,面试官究竟想要问你什么了吧~

当然,只懂了这些还远远不够~

很多同学在面试的时候也有面试官会问promise和setTimeout的执行顺序问题

比如这个面试题:

 setTimeout(() => {
    console.log(4)
  }, 0);
  new Promise((resolve) =>{
    console.log(1);
    for (var i = 0; i < 10000000; i++) {
      i === 9999999 && resolve();
    }
    console.log(2);
  }).then(() => {
    console.log(5);
  });
  console.log(3);

这个题的答案是1, 2, 3, 5, 4

是不是很神奇?

相信很多同学已经开始不清楚为什么会这样了

因为我们还需要学习宏任务和微任务


3. 宏任务(macro task) 和 微任务(micro task)

任务队列又分为 macro-task(宏任务)micro-task(微任务) ,在最新标准中,它们被分别称为 taskjobs

  • macro-task(宏任务)大概包括:script(整体代码), setTimeout, setInterval, setImmediate(NodeJs), I/O, UI rendering
  • micro-task(微任务)大概包括: process.nextTick(NodeJs), Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • 来自不同任务源的任务会进入到不同的任务队列。其中 setTimeoutsetInterval 是同源的。

事实上,事件循环决定了代码的执行顺序,从全局上下文进入函数调用栈开始,直到调用栈清空,然后执行所有的micro-task(微任务),当所有的micro-task(微任务)执行完毕之后,再执行macro-task(宏任务),其中一个macro-task(宏任务)的任务队列执行完毕(例如setTimeout 队列),再次执行所有的micro-task(微任务),一直循环直至执行完毕。

解析

现在我就开始解析上面的代码。

  • 第一步,整体代码 script 入栈,并执行 setTimeout 后,执行 Promise

a2c5a97b0db9e0f375abdd000e9fec47.png
  • 第二步,执行时遇到 Promise 实例,Promise 构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-taskPromise队列中去。

1c4c186682b663b1371eb27350199c1a.png

86aa402610d8f90d851c3e4a3d10635d.png
  • 第三步,调用栈继续执行宏任务 app.js,输出3并弹出调用栈,app.js 执行完毕弹出调用栈:

793118fab0aab2c6f2db3198683415da.png

13e6d27c2897d9a0e544c383bbacf49a.png
  • 第四步,这时,macro-task(宏任务)中的 script 队列执行完毕,事件循环开始执行所有的 micro-task(微任务)

6f08dad0ed6326ab3c36372acac38975.png
  • 第五步,调用栈发现所有的 micro-task(微任务) 都已经执行完毕,又跑去macro-task(宏任务)调用 setTimeout 队列:

b57d0e0f4502815756bb6979dab15305.png
  • 第六步,macro-task(宏任务) setTimeout 队列执行完毕,调用栈又跑去微任务进行查找是否有未执行的微任务,发现没有就跑去宏任务执行下一个队列,发现宏任务也没有队列执行,此次调用结束,输出内容1,2,3,5,4

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。

总结

  1. 不同的任务会放进不同的任务队列之中。
  2. 先执行macro-task,等到函数调用栈清空之后再执行所有在队列之中的micro-task
  3. 等到所有micro-task执行完之后再从macro-task中的一个任务队列开始执行,就这样一直循环。
  4. 宏任务和微任务的队列执行顺序排列如下:
  5. macro-task(宏任务)script(整体代码), setTimeout, setInterval, setImmediate(NodeJs), I/O, UI rendering
  6. micro-task(微任务): process.nextTick(NodeJs), Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值