什么是 Event Loop?

若你了解过JavaScript,你一定知道JavaScript是一种单线程语言,为什么JavaScript是单线程呢?为什么不使用多线程呢?JavaScript作为浏览器脚本语言(虽然现在也在后端挺流行的),JavaScript的主要用途是与用户互动,以及操作DOM。若使用多线程就会导致一些问题,比如更新丢失等问题,当一个线程要删除DOM的时候另一个线程要更改它,那浏览器该如何操作呢。所以JavaScript为了避免多线程带来的一系列问题采用了单线程的运行机制。

而若只是单纯的同步单线程的执行便会导致JS运行到某个需要等待的位置时就会造成假死状态,比如当JS要从网络中获取一张巨大的图片,发起了HTTP请求,在等待HTTP请求中若是不采用某种机制来处理的话就会导致卡住的假死状态,我们可以用Java来模拟一下:

import java.util.Date;

public class Demo01 {
   public static void main(String[] args) {
      // 打印该句执行的时间
      System.out.println("begin: " + new Date().getTime());
      try {
         // 利用sleep模拟请求时的状态
         Thread.sleep(5000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      // 打印该句执行的时间
      System.out.println("end: " + new Date().getTime());
   }
}

从上面的运行结果可以看到,同步运行的时候Java在begin和end中间隔了非常久的时间,程序也在那时候被阻塞住了,而UI是不能被阻塞的否则会严重影响用户体验,所以JS采用异步来防止这种情况发生。

当JS线程执行到需要异步的操作的时候就会把该任务发到任务队列,然后继续向下执行,当所有的同步代码都执行完毕的时候,JS线程就会从任务队列读取任务并执行,若遇到异步操作就继续入队。。。如此往复,这就是Event Loop(事件循环)

MacroTask和MicroTask

MacroTask(宏任务)MicroTask(微任务),宏任务是一个大任务,在一个宏任务包含着许多个微任务,我们可以这样认为宏任务队列就代表着事件循环,而宏任务中的Promise还会产生异步代码,JavaScript必须保证这些异步代码在一个宏观任务中完成,所以每个宏任务中包含了一个微任务队列。在宏任务中产生的异步操作会添加在微任务队列的尾部,在本tick中完成。举个🌰吧:

  • 执行html中引用的js代码,碰到宏任务的时候就把宏任务添加到宏任务队列,遇到微任务就在这个tick中执行完毕,全局的js执行完毕后调用栈会清空
  • 从宏任务队列中取出队首任务,执行,遇到微任务就添加到该宏任务中的微任务队列中,在本tick中完成,遇到宏任务就添加到宏任务队列尾部。
  • 取出该宏任务中的微任务,逐个执行,如果遇到宏任务就添加到宏任务队列,遇到微任务就添加到微任务队列,直到该宏任务中微任务队列清空,则代表该宏任务完成
  • 取出下一个宏任务,执行。。。重复2-3步
  • 取出下一个宏任务,执行。。。重复2-3步

Promise

Promise是ES6新增的一种异步解决方案,它的运行方式是当需要进行I/O,等待等异步操作的时候,不返回结果而是返回一个promise(”承诺”),当这个承诺完成的时候,即状态变为fulfilled或者rejected,这个promise就定格了,也可以认为返回值是一样的了,你可以在任何位置任何时间利用then得到这个结果(返回值),或许这有点难以理解,那就举个🌰吧:

var re = "foo"

var promise1 = new Promise(function(resolve, reject) {
	setTimeout(function() {
    	resolve(re);
	}, 300);
});

promise1.then(function(value) {
	console.log("one call value: " + value);
	console.log("one vall re prev: " + re);
	re="foo2";
	console.log("one call re: " + re);
});

promise1.then(function(value) {
	console.log("two call value: " + value);
  	console.log("two call re: " + re)
})

console.log(promise1);

// > [object Promise]
// > "one call value: foo"
// > "one vall re prev: foo"
// > "one call re: foo2"
// > "two call value: foo"
// > "two call re: foo2"

从上面的运行结果可以看到re确实在第一次调用promise1的时候被修改为foo2,但是当第二次调用promise时的value并没有跟着改变,也就是说promise不会再调用第二次,而是直接返回结果。

async/await

ES7中添加了async和await的关键字,async返回的必定时Promise,可以理解为就是异步函数,await是等待Promise。

async可以使一个函数成为异步函数,返回Promise,我们可以把async认为使new Promise的语法糖,所以当函数return值的时候return的不是值而是Promise,若要得到async中return的值就需要使用then。

var fun = async function foo(){
  return "hello";
}

var re = fun();
console.log(re);
re.then(value => console.log(value));

// > [object Promise]
// > "hello"

await是等待后面东西,可以是Promise,可以是值,可以是表达式,当可以直接得到值的时候await会立即返回值,但若是Promise,await就会将js阻塞住,直到Promise兑现,有了await,我们可以把异步的js写成同步的js,可以有效的解决回调地狱。

一个有用的🌰

console.log('sync1');

setTimeout(function () { //1
    console.log('sync_timeout1')
}, 0);

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () { //2
        console.log('pro_new_timeout')
    }, 0);
    console.log('pro_new');
    resolve();
});


promise.then(() => {
    console.log('pro_then');
    setTimeout(() => { //3
        console.log('pro_timeout');
    }, 0)
})

setTimeout(function () { //4
    console.log('sync_timeout2')
}, 0);
console.log('sync2');

看到这个代码,是不是很晕,先不要放到JS中运行,让我们一起来看看这个的输出,你可以先不看以下的步骤自己思考一下,或许能得到不小收获。

首先,sync1的console.log肯定是第一个输出的,调用栈先进了console,然后出栈,控制台输出sync1

然后程序来到了第一个setTimeout,这是一个异步宏任务,所以放到宏任务队列中,然后跳过该setTimeout。MacroTask Queue:[setTimeout-1]

接下来程序来到了new promise,new promise是同步的,所以会进入到function中,遇到第二个setTimeout,此时将这个setTimeout放到宏任务队列中,然后跳过setTimeout,执行到console.log,输出pro_new,接着遇到resolve,是微任务将其放到微任务队列中,然后退出function。MacroTask Queue:[setTimeout-1,setTimeout-2],MicroTask Queue:[resolve]

接着遇到了promise的then,这是属于resolve的回调,当resolve状态改变的时候才执行,所以跳过该部分。MacroTask Queue:[setTimeout-1,setTimeout-2],MicroTask Queue:[resolve]

然后又遇到了一个setTimeout,同样将其放到宏任务队列中。MacroTask Queue:[setTimeout-1,setTimeout-2,setTimeout-4],MicroTask Queue:[resolve]

接着遇到了最后一个console.log,输出sync2,此时同步代码已经执行完毕。

微任务列表不为空,所以需要在这个tick中执行,不能先取宏任务,调用resolve的then,输出pro_then,同时将setTimeout放入宏任务队列。MacroTask Queue:[setTimeout-1,setTimeout-2,setTimeout-4,setTimeout-3],MicroTask Queue:[]

微任务队列空了,从宏任务中取出队首的任务,即setTimeout-1,执行后输出sync_timeout1

取出队首任务,setTimeout-2,输出pro_new_timeout

取出队首任务,setTimeout-4,输出sync_timeout2

取出队首任务,setTimeout-3,输出pro_timeout,此时宏任务列表和微任务队列为空,js引擎进入等待状态。

  • sync1
  • pro_new
  • sync2
  • pro_then
  • sync_timeout1
  • pro_new_timeout
  • sync_timeout2
  • pro_timeout

你们猜对了吗?反正我第一次是没猜对(逃

结语

学了好久的前端,之前对异步只处于会用的状态,前几天刚了解了一下JS的异步,刚好好久没写过文章了,便自己整理了写成一篇文章,输出才是最好的学习,其实是为了水文章(逃。( ̄y▽, ̄)╭

说点什么
本博客评论规则(评论规则什么的都是浮云,小声
支持Markdown语法
在"浅谈浏览器Event Loop"已有4条评论
Loading...