作者:Sukhjinder Arora
译者:前端小智
来源:medium
为了保证的可读性,本文采用意译而非直译。
想阅读更多优质文章请猛戳 GitHub 博客, 一年百来篇优质文章等着你!
JS
是一门单线程的编程语言,这就意味着一个时间里只能处理一件事,也就是说 JS 引擎一次只能在一个线程里处理一条语句。
虽然单线程简化了编程代码,因为这样咱们不必太担心并发引出的问题,这也意味着在阻塞主线程的情况下执行长时间的操作,如网络请求。
想象一下从 API 请求一些数据,根据具体的情况,服务器需要一些时间来处理请求,同时阻塞主线程,使网页长时间处于无响应的状态。这就是引入异步 JS 的原因。使用异步 (如 回调函数、promise
、async/await
), 可以不用阻塞主线程的情况下长时间执行网络请求。
了解异步的工作方式之前,咱们先来看看同步是怎么样工作的。
同步 JS 是如何工作的?
在深入研究异步 JS
之前,先来了解同步 JS
代码在 JavaScript
引擎中执行情况。例如:
const second = () => {console.log('Hello there!');
}
const first = () => {console.log('Hi there!');
second();
console.log('The End');
}
first();
要理解上述代码如何在 JS
引擎中执行,咱们必须理解什么是 执行上下文 和调用栈(也称为执行堆栈)。
函数代码在函数执行上下文中执行,全局代码在全局执行上下文中执行。每个函数都有自己的执行上下文。
调用栈
调用堆栈顾名思义是一个具有 LIFO
(后进先出) 结构的堆栈,用于存储在代码执行期间创建的所有执行上下文。
JS
只有一个调用栈,因为它是一种单线程编程语言。调用堆栈具有 LIFO
结构,这意味着项目只能从堆栈顶部添加或删除。
回到上面的代码,尝试理解代该码是如何在 JS
引擎中执行。
const second = () => {console.log('Hello there!');
}
const first = () => {console.log('Hi there!');
second();
console.log('The End');
}
first();
这里发生了什么?
当执行此代码时,将创建一个全局执行上下文 (由 main() 表示)并将其推到调用堆栈的顶部。当遇到对 first()
的调用时,它会被推送到堆栈的顶部。
接下来,console.log('Hi there!')
被推送到堆栈的顶部,当它完成时,它会从堆栈中弹出。之后,我们调用 second()
,因此second()
函数被推到堆栈的顶部。
console.log('Hello there!')
被推送到堆栈顶部,并在完成时弹出堆栈。second()
函数结束,因此它从堆栈中弹出。
console.log(“the End”)
被推到堆栈的顶部,并在完成时删除。之后,first()
函数完成,因此从堆栈中删除它。
程序在这一点上完成了它的执行,所以全局执行上下文 (main()) 从堆栈中弹出。
异步 JS 是如何工作的?
现在咱们已经对调用堆栈和同步 JAS
的工作原理有了基本的了解,回到异步 JS
上。
阻塞是什么?
假设咱们正在以同步的方式进行图像处理或网络请求。例如:
const processImage = (image) => {
/**
* doing some operations on image
**/
console.log('Image processed');
}
const networkRequest = (url) => {
/**
* requesting network resource
**/
return someData;
}
const greeting = () => {console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
做图像处理和网络请求需要时间,当 processImage()
函数被调用时,它会根据图像的大小花费一些时间。
processImage()
函数完成后,将从堆栈中删除它。然后调用 networkRequest()
函数并将其推入堆栈。同样,它也需要一些时间来完成执行。
最后,当 networkRequest()
函数完成时,调用 greeting()
函数。
因此,咱们必须等待函数如 processImage()
或networkRequest()
完成。这意味着这些函数阻塞了调用堆栈或主线程。因此,在执行上述代码时,咱们不能执行任何其他操作,这是不理想的。
解决办法是什么?
最简单的解决方案是异步回调,各位使用异步回调使代码非阻塞。例如:
const networkRequest = () => {setTimeout(() => {console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
这里使用了 setTimeout
方法来模拟网络请求。请记住 setTimeout
不是 JS
引擎的一部分,它是 Web Api 的一部分。
为了理解这段代码是如何执行的,咱们必须理解更多的概念,比如事件轮询和回调队列(或消息队列)。
事件轮询、web api 和消息队列不是 JavaScript
引擎的一部分,而是浏览器的 JavaScript
运行时环境或 Nodejs JavaScript 运行时环境的一部分(对于 Nodejs)。在 Nodejs 中,web api 被 c /c++ api 所替代。
现在让我们回到上面的代码,看看它是如何异步执行的。
const networkRequest = () => {setTimeout(() => {console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');
当上述代码在浏览器中加载时,console.log('Hello World')
被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest()
的调用,因此将它推到堆栈的顶部。
下一个 setTimeout()
函数被调用,因此它被推到堆栈的顶部。setTimeout()
有两个参数:
- 1) 回调和
- 2) 以毫秒 (ms) 为单位的时间。
setTimeout()
方法在 web api 环境中启动一个 2s 的计时器。此时,setTimeout()
已经完成,并从堆栈中弹出。cosole.log(“the end”)
被推送到堆栈中,在完成后执行并从堆栈中删除。
同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。
事件轮询
事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。
在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。
然后是 console.log(“Async Code”)
被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。
消息队列还包含来自 DOM 事件 (如单击事件和键盘事件) 的回调。例如:
document.querySelector('.btn').addEventListener('click',(event) => {console.log('Button Clicked');
});
对于 DOM 事件,事件侦听器位于 web api 环境中,等待某个事件 (在本例中单击 event) 发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。
同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。
延迟函数执行
咱们还可以使用 setTimeout
来延迟函数的执行,直到堆栈清空为止。例如
const bar = () => {console.log('bar');
}
const baz = () => {console.log('baz');
}
const foo = () => {console.log('foo');
setTimeout(bar, 0);
baz();}
foo();
打印结果:
foo
baz
bar
当这段代码运行时,第一个函数 foo()
被调用,在 foo
内部我们调用 console.log('foo')
,然后setTimeout()
被调用,bar()
作为回调函数和时 0
秒计时器。
现在,如果咱们没有使用 setTimeout
, bar()
函数将立即执行,但是使用 setTimeout
和 0
秒计时器,将 bar
的执行延迟到堆栈为空的时候。
0
秒后,bar()
回调被放入等待执行的消息队列中,但是它只会在堆栈完全空的时候执行,也就是在 baz
和foo
函数完成之后。
ES6 任务队列
我们已经了解了异步回调和 DOM 事件是如何执行的,它们使用消息队列存储等待执行所有回调。
ES6 引入了任务队列的概念,任务队列是 JS
中的 promise
所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise
作业将在消息队列中的回调之前执行,例如:
const bar = () => {console.log('bar');
};
const baz = () => {console.log('baz');
};
const foo = () => {console.log('foo');
setTimeout(bar, 0);
new Promise((resolve, reject) => {resolve('Promise resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
baz();};
foo();
打印结果:
foo
baz
Promised resolved
bar
咱们可以看到 promise
在 setTimeout
之前执行,因为 promise
响应存储在任务队列中,任务队列的优先级高于消息队列。
小结
因此,咱们了解了异步 JS
是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JS
运行时环境。虽然成为一名出色的 JS
开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的。
代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
原文:https://blog.bitsrc.io/unders…
交流
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq44924588…
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复 福利,即可看到福利,你懂的。