JavaScript事件循环(Event loop)、宏任务微任务

62 阅读5分钟

讲解

线程和进程

概念

进程与线程

进程:程序的一次执行,占有一次独立的内存区域

线程:CPU的基本调用单位,是程序执行的一个完整流程

关系:一个进程至少拥有一个运行的线程(主线程),一个进程可以用多个线程,多个进程数据是不共享。

JavaScript

js一门单线程的非阻塞的脚本语言

单线程:需要一个任务执行完成后才会去执行另外一个任务。

非阻塞:实现方式是通过异步的方式来实现的,比如说:发现当我们使用定时器的时候也并没有影响后面的任务。主要实现方式是通过异步来实现的。

看一下代码的执行结果:

const f2 = async () => {
  console.log('f2');
};

const f1 = async () => {
  await f2();
  console.log('f1');
};

console.log('正常1');

f1();

setTimeout(() => {
  console.log('定时器');
});

console.log('正常2');

// 正常1 f2 正常2 f1 定时器

为什么js要设计成单线程的语言呢?

我们从另外的角度考虑一下,要是js是一个多线程的语言。一个线程为一个dom添加了一个点击事件,但是另外的一个线程删除了这个dom 节点。那就会有很多莫名其妙的问题,靠人为的保证并不可靠。因此为了避免这个问题js只用一个主线程来执行代码,保证了程序的一致性。

为什么js要设计成单线程的语言呢?

执行栈与事件队列

执行栈:当我们调用一个方法的时候,js会生成一个和这个方法对应的执行环境(context),又被叫做执行上下文。

执行环境中包含这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域的变量以及这个作用域的this对象。当一系列方法被依次嗲用的时候,因为js是单线程,同一时间只能执行一个方法,于是这些方法被排列在一个地方,这个地方就是执行栈。

事件队列:当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码安在执行顺序放到执行栈中,然后从头开始执行。

同步方法执行

当执行的是一个方法的时候,那么js会向这个执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。等这个执行环境中的代码执行完毕并返回结果后,js就会退出这个执行环境并销毁,回到上一个方法的执行环境,这个过程反复进行,直到执行环境中的代码全部执行完成,一个方法执行会向执行环境添加这个方法的执行环境,这个执行环境可以调用其他方法,从结果来看就是向执行环境中添加一个执行环境。这个过程可以无限的执行下去,除非栈溢出(超过了内存的最大值)

异步方法执行

js引擎遇到一个异步时间的时候, 并不会一直等待返回结果,而是先将这个事件挂起,继续执行执行栈中的其他任务,当第一个异步返回结果的时候,js会将这个事件加入到与当前执行栈不同的另外一个队列,我们称为 事件队列。

事件循环

被放在事件队列中并不会立即执行回调,而是等待当前执行栈中的所有任务都执行完毕,主线程处于空闲的状态的情况下,主线程会查看事件队列中是否还存在任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件放在回调执行栈中,然后执行代码,如此反复,形成了一个无限循环就是我们说的事件循环(event loop)

setTimeout(() => {
  console.log('3');
}, 0);

Promise.resolve(2).then((value) => console.log(value));

console.log("1")

// 1 2 3

任务

宏任务(macro-task):发起者是宿主(node、浏览器)

  • script
  • setTimeout
  • setInterval
  • UI事件
  • I/O (nodejs)
  • ajax 微任务(micro-task):发起者是Js引擎
  • Promise
  • Mutation

微队列优先级是高于宏队列优先级

setTimeout(() => {
  console.log('4');
}, 0);

setTimeout(() => {
  console.log('5');
}, 0);

Promise.resolve(2).then((value) => console.log(value));

Promise.resolve(3).then((value) => console.log(value));

console.log('1');

// 1 2 3 4 5 
setTimeout(() => {
  console.log('4');
  Promise.resolve(3).then((value) => console.log(value));
}, 0);

setTimeout(() => {
  console.log('5');
}, 0);

Promise.resolve(2).then((value) => console.log(value));

console.log('1');

// 1 2 4  3 5

这两端代码执行结果的差异是因为,每执行一个宏任务,都会检查微队列中是否有待执行的回调,优先执行微任务,简言之,微任务是可以插队的。

async方法执行时,遇到await会立即执行表达式,async表达式定义的函数是立即执行的,await表达式后面的代码放在微任务执行,包括赋值。

例子(实战)

一些简单的小代码验证我们的学习成果

const test2 = async () => {
  console.log('test2');
};

const test1 = async () => {
  console.log('test1 begin');
  await test2();
  console.log('test1 end');
};

console.log('script begin');
test1();
console.log('script end');

结果:

image.png

console.log(1)

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {console.log(3)});
});

new Promise((resolve,reject)=>{
  console.log(4);
  resolve(5)
}).then(data=>{console.log(data)})

setTimeout(() => {
  console.log(6);
});

console.log(7);

// 1 4 7 5 2 3 6
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}

// 1010 

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

// 0 1 2 3 4 5 6 7 8 9

使用立即执行函数的方法来避免这个问题

for (var i = 0; i < 10; i++) {
  ((i)=>{setTimeout(() => {
    console.log(i);
  }, 0)})(i);
}
// 0 1 2 3 4 5 6 7 8 9

分析:尽管循环执行结束,i值已经变成了3。但因遇到了自执行函数,当时的i值已经被 lockedIndex锁住了。也可以理解为 自执行函数属于for循环一部分,每次遍历i,自执行函数也会立即执行。所以尽管有延时器,但依旧会保留住立即执行时的i值。

最后写一个长一点的小例子

const async2 = () => {
  console.log('async2');
};
const async1 = async () => {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
};

console.log('script start');

setTimeout(function () {
  console.log('settimeout');
});

async1();

new Promise(function (resolve) {
  console.log('promise1');
  resolve(1);
}).then(function () {
  console.log('promise2');
});

console.log('script end');

image.png

引用

www.jianshu.com/p/de481db8f… copyfuture.com/blogs-detai…