EventLoop

单线程

JavaScript 语言和执行环境是单线程。即同一时间,只能处理一个任务。

具体来说,所谓单线程,是指 JS 引擎中负责解释和执行 JavaScript
代码的线程只有一个,也就是一次只能完成一项任务,这个任务执行完后才能执行下一个。所有的任务都需要排队

JS 为何要被设计为单线程呢?原因如下:

  • 首先是历史原因,在最初设计 JS
    这门语言时,多进程、多线程的架构并不流行,硬件支持并不好。

  • 其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。

  • 而且,如果多个线程同时操作同一个
    DOM,在多线程不加锁的情况下,会产生冲突,最终会导致 DOM
    渲染的结果不符预期。

所以,为了避免这些复杂问题的出现,JS 被设计成了单线程语言。

同步任务和异步任务

如果当前正在执行的任务很耗时,它就会阻塞其他正在排队的任务。为了解决这个问题,JS
在设计之初,将任务分成了两类:同步任务、异步任务。

  • 同步任务:在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行下一个任务。

  • 异步任务:不进入主线程、而是进入任务队列(Event
    Queue)的任务。只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

代码举例:

console.log(‘同步任务1’);

setTimeout(() => {

console.log(‘异步任务’);

}, 1000);

console.log(‘同步任务2’);

打印结果是:

同步任务1

同步任务2

异步任务

代码解释:第一行代码是同步任务,会立即执行;定时器里的回调函数是异步任务,需要等
1
秒后才会执行。假如定时器里的代码是同步任务,那需要等待1秒后,才能执行最后一行代码console.log(‘同步任务2’),也就是造成了主线程里的同步任务阻塞,这不是我们希望看到的。

比如说,网络图片的请求,就是一个异步任务。前端如果同时请求多张网络网络图片,谁先请求完成就让谁先显示出来。假如网络图片的请求做成同步任务,那就会出大问题,所有图片都得排队加载,如果第一张图片未加载完成,就得卡在那里,造成阻塞,导致其他图片都加载不出来。页面看上去也会很卡顿,这肯定是不能接受的。

前端使用异步的场景

什么时候需要等待,就什么时候用异步。常见的异步场景如下:

  • 1、定时器:setTimeout(定时炸弹)、setInterval(循环执行)

  • 2、事件绑定(比如说,按钮绑定点击事件之后,用户爱点不点。我们不可能卡在按钮那里,什么都不做。所以,应该用异步)

  • 3、网络请求(含接口请求):ajax 请求、网络图片加载

  • 4、ES6 中的 Promise

现在的大部分软件项目,都是前后端分离的。后端生成接口,前端请求接口。前端发送
ajax
请求,向后端请求数据,然后等待一段时间后,才能拿到数据。这个请求过程就是异步任务。

接口调用的方式

js 中常见的接口调用方式,有以下几种:

  • 原生 ajax、基于 jQuery 的 ajax

  • Promise

  • Fetch

  • axios

下一篇文章,我们重点讲一下接口调用里的 Ajax,然后在 ES6
语法中学习 Promise。在这之前,我们需要先了解同步任务、异步任务的事件循环机制。

事件循环机制(重要)

  1. 同步任务会由Javascript主线程依次执行

  2. 异步任务会委托给宿主环境执行

  3. 宿主环境会将执行完的异步任务对应的回调函数排入队列中

  4. 主线程完成所有同步任务之后,就会开始依次执行异步任务的回调函数

  5. 如此循环反复

其中3~4这个过程就是EventLoop

多次异步调用的顺序

  • 多次异步调用的结果,顺序可能不同步。

  • 异步调用的结果如果存在依赖,则需要通过回调函数进行嵌套。

定时器:代码示例

掌握了上面的事件循环原理之后,我们来看几个例子。

举例 1

console.log(1);

setTimeout(() => {

console.log(2);

}, 1000);

console.log(3);

console.log(4);

打印结果:

1

3

4

2

解释:先等同步任务执行完成后,再执行异步任务。

举例 2(重要)

如果我把上面的等待时间,从 1 秒改成 0 秒,你看看打印结果会是什么。

console.log(1);

setTimeout(() => {

console.log(2);

}, 0);

console.log(3);

console.log(4);

打印结果:

1

3

4

2

可以看到,打印结果没有任何变化,这个题目在面试中经常出现,考的就是 setTimeout(()=>
{},
0)会在什么时候执行。这就需要我们了解同步任务、异步任务的执行顺序,即前面讲到的事件循环机制

解释:先等同步任务执行完成后,再执行异步任务。

同理,我们再来看看下面这段伪代码:

setTimeout(() => {

console.log(‘异步任务’);

}, 2000);

// 伪代码

sleep(5000); //表示很耗时的同步任务

上面的代码中,异步任务不是 2
秒之后执行,而是等耗时的同步任务执行完毕之后,才执行。那这个异步任务,是在
5 秒后执行?还是在 7 秒后执行?这个作业,留给读者你来思考~

举例 3(经典面试题)

console.log(‘A’);

thenFs.readFile(‘./files/1.txt’, ‘utf-8’).then(dataStr => {

console.log(‘B’);

})

setTimeout(() => {

console.log(‘C’);

}, 0);

console.log(‘D’);

输出结果:

A

D

C

B

其中C是异步任务,先托管给宿主环境,等到A和D执行完成之后,发现setTimout不需要等待,这时才打出C。

举例 4(较真系列)

setTimeout(() => {

console.log(‘异步任务’);

}, 1000);

上面的代码中,等到 1 秒之后,真的会执行异步任务吗?其实不是。

在浏览器中, setTimeout()/ setInterval()
的每调用一次定时器的最小时间间隔是4毫秒,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的
setInterval 的回调函数阻塞导致的。

上面的案例中,异步任务需要等待 1004 毫秒之后,才会从 Event Table 进入到
Event Queue。这在面试中也经常被问到。

异步任务举例

例 1:加载图片

// 加载图片的异步任务

function loadImage(file, success, fail) {

const img = new Image();

img.src = file;

img.onload = () => {

// 图片加载成功

success(img);

};

img.onerror = () => {

// 图片加载失败

fail(new Error(‘img load fail’));

};

}

loadImage(

‘images/qia nguyihao.png’,

(img) => {

console.log(‘图片加载成功’);

document.body.appendChild(img);

img.style.border = ‘solid 2px red’;

},

(error) => {

console.log(‘图片加载失败’);

console.log(error);

}

);

例 2:定时器计时,移动 DOM 元素

// 函数封装:定义一个定时器,每间隔 delay 毫秒之后,执行 callback 函数

function myInterval(callback, delay = 100) {

let timeId = setInterval(() => callback(timeId), delay);

}

myInterval((timeId) => {

// 每间隔 500毫秒之后,向右移动 .box 元素

const myBox = document.getElementsByClassName(‘box’)[0];

const left = parseInt(window.getComputedStyle(myBox).left);

myBox.style.left = left + 20 + ‘px’;

if (left > 300) {

clearInterval(timeId);

// 每间隔 10 毫秒之后,将 .box 元素的宽度逐渐缩小,直到消失

myInterval((timeId2) => {

const width = parseInt(window.getComputedStyle(myBox).width);

myBox.style.width = width - 1 + ‘px’;

if (width <= 0) clearInterval(timeId2);

}, 10);

}

}, 200);

参考链接


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!