前言:
这段时间在学习 vue 的同时, 又将 JavaScript 异步方面的知识又复习了一遍, 前前后后也看了不少的文章, 但感觉有点混乱, 对异步也没有一个相对体系化的认识因此准备写几篇文章来好好的来理清一下思路, 如有错误疏漏请指出, 不胜感激!
单线程的 JavaScript
大家在初学 JavaScript 的时候应该或多或少都知道 JavaScript 是一门单线程的弱类型语言而 JavaScript 之所以设计成单线程其实与它的用途有关: 由于 JavaScript 的早期的主要用途是与用户交互以及操作 DOM, 因此如果设计成多线程并行就会带来很多复杂和不可控的同步问题, 比如当两个不同的线程一个是要往一个节点添加内容, 另一个则是要删除这个节点, 这时浏览器就懵逼了
由于 JavaScript 是单线程的, 这就意味着, JavaScript 中所有的任务都需要排队依次执行这样说起来很简单, 但很多时候我们在写程序的时候就可能会意识到一个问题, 那就是程序中将来执行的代码并不一定是在现在运行的代码执行完之后就立即执行比如这样:
- console.log(1);
- setTimeout(function() {
- console.log(2);
- },1000);
- console.log(3);
- // 1 3 2
看到上面的代码, 很多只想着单线程的哥们可能会毫不犹豫的大喊: 1 2 3! 但现实总是骨感的, 人生如此, 爱情如此, 代码也是如此正确的打印顺序应该是: 1 3 2 这是由于 JavaScript 的设计者在设计之初就考虑到, 单线程可能会由于运算量过大或加载耗时过长等原因, 而使后面的任务只能痴痴等待, 不能立即执行, 而导致我们常说的 IO 操作 (耗时但 CPU 处于闲置状态) 因此 JavaScript 设计者就将所有任务分以下两种任务以解决这个问题:
同步任务(在主线程中, 只有前面的代码执行完毕后, 后面的才能执行)
异步任务(从主线程提出来, 异步执行, 当执行完毕后在任务队列中放入一个事件, 等主线程的任务执行完毕后再从任务队列中读取该任务的事件, 并执行该任务)
主线程与任务队列的示意图如下(转自阮一峰老师的 JavaScript 运行机制详解: 再谈 Event Loop):
image
事件循环(Event Loop)
前面我们提到, 当 JavaScript 在异步任务完成后会通知主线程该任务可以执行了, 那么又是如何 通知的呢? 其实用一句话就可以去描述这个过程:
工作线程将信息放到任务队列中, 主线程则通事件循环过程去读取完成的消息
让我们再来看这段代码:
- console.log(1);
- setTimeout(function() {
- console.log(2);
- },1000);
- console.log(3);
- // 1 3 2
其实它会经历一下几个步骤:
打印 1
调用 setTimeout, 发现是一个异步任务, 从主线程中提出, 进行异步运行
打印 3
异步任务运行完毕, 工作线程在任务队列中放置一个事件
主线程所有同步事件执行完毕后, 通过事件循环读取事件, 然后将异步任务放入主线程最后端执行
打印 2
总结起来, JavaScript 的代码执行机制其实就三点:
所有同步任务都在主线程上执行, 形成一个执行栈(execution context stack)
主线程之外, 还存在一个 "任务队列"(task queue)只要异步任务有了运行结果, 就在 "任务队列" 之中放置一个事件
一旦主线程的栈中的所有同步任务执行完毕, 系统就会读取任务队列, 选择需要首先执行的任务然后执行
实际上, 简单来说, 主线程在第三步就是从任务队列里面取事件执行事件, 执行完毕; 再取事件再执行事件... 这样不断取事件执行事件的循环机制就叫做事件循环机制 (需要注意的是, 当任务队列为空时, 就会等待, 直到任务队列变成非空) 其基本逻辑如下:
- while (queue.waitForMessage()) {
- queue.processNextMessage();
- }
常见的异步事件
在我们的日常开发中, 比较常见的异步事件主要是以下三种:
DOM 操作(在用户执行操作后进入任务队列)
网络请求(在网络响应后进入任务队列)
定时器(在规定时间到达后进入任务队列)
现在让我们在看看具体的实例吧:
DOM 操作
- console.log(1);
- document.getElementById('btn').addEventListener('click',function() {
- console.log(2);
- });
- console.log(3);
- //1
- //3
- // 点击后
- //2
上面的代码很容易理解, 先后打印 1 和 3 当用户进行点击后才会执行异步任务, 打印出 2
网络请求
而在网络请求方面, 通常我们会遇到以下两种情况:
进行 ajax 请求
动态 < img > 加载
先让我们来看看 ajax 请求:
- console.log('1');
- $.get(''./wozeishuai.json',function(data) {
- console.log(data);
- });
- console.log(2);
- // 1
- // 2
- // ?
首先两个同步任务会被依次执行, 打印出 1 和 2 而 data 的打印则会视情况而定, 如果 ajax 请求成功, 那么 data 就会在 2 后面被打印出来; 但如果请求不成功, data 就不会被打印出来
还有就是动态的 < img > 的加载所产生的异步问题, 在这方面我们可能会遇到这种情况:
- console.log(1);
- let img = document.creatElement('img');
- img.onload = function() {
- console.log(2);
- };
- img.src = '/sky.png';
- console.log(3);
- //1
- //3
- //?
同理, 当我们执行上面的代码的时候, 我们首先依次打印出 1,3 然后就需要等待 img 的加载, 这同样需要一个过程如果加载成功就会打印出 2, 如果加载失败, 那么 2 就不会被打印出来对于 img 的加载问题, 通常我们还有可能遇到这种情况:
document.getElementsByTagNames('img')[0].width
乍看起来这段代码并没有什么问题, 好像并没有存在异步的问题, 一切都应该是你期望的那样进行着但当你执行时, 却惊讶的发现, 取得的竟然 width 是 0! 然后整个人都不好了, 心态崩了, 怎么也想不明白为什么会是这样 (没错就是刚学编程的我) 其实这个问题也很好理解, 因为 < img > 的加载是需要时间的, 因此会被浏览器归入异步任务之中, 而这条语句是同步语句, 会被主线程依次执行, 当这条语句执行完毕后, img 才会被加载完成进入主线程, 所以我们不能取得正确的 width 遇到这种情况, 我们可以改写代码, 使其能够取到正确的 width:
- document.getElementsByTagNames('img')[0].onload = function(){
- console.log(this.width); // 打印 width
- };
定时器
定时器我们在上面的示例中已经有提到了, 就是这个:
- console.log(1);
- setTimeout(function() {
- console.log(2);
- },1000);
- console.log(3);
- // 1 3 2
对于定时器, 主要有以下三个用处:
让浏览器渲染当前的变化(很多浏览器 UI 渲染和 JavaScript 执行是放在一个线程中, 当线程阻塞时会导致界面无法更新渲染)
重新评估 script is running too long 的警告
改变代码的执行顺序
还有一点我们得要额外注意那就是当在零延迟调用 setTimeout 时, 它并不会是真正的零延迟, 它的调用取决于队列里正在等待的消息数量
- (function() {
- console.log(1);
- setTimeout(function cb() {
- console.log(2);
- });
- console.log(3);
- setTimeout(function cb1() {
- console.log(4);
- }, 0);
- console.log(5);
- })();
- //1 3 5 2 4
其他要点
浏览器不是单线程的
虽然 JavaScript 通常运行在浏览器中, 且是单线程的, 且每个 window 都有一个 JavaScript 线程但浏览器并不是单线程的, 例如 webkit 或是 Gecko 引擎, 都可能有如下线程:
JavaScript 引擎线程
浏览器 UI 渲染线程
浏览器事件触发线程
HTTP 请求线程
阻塞问题
因为 JavaScript 处理 I/O 时, 通常可以通过事件和回调来执行, 因此当一个应用正等待 IndexedDB 查询返回或者一个 XHR 请求返回时, 它仍然可以处理其它事情, 所以通常来说 JavaScript 是不会出现阻塞的但凡事都有例外, 比如这样:
- console.log(1);
- alert('hello,world');
- console.log(2);
执行上面的代码的时候, 它并不会依次执行下去, 而是先打印 1, 然后跳出一个弹窗, 只有当你点击确定之后, 才会执行后面的代码打印 2 出来具有这种阻塞效果的有 alert 之类的弹窗和同步 XHR, 这需要在实践时额外注意, 以避免出现阻塞的问题
来源: http://www.jianshu.com/p/fb9a0c9bf716