JavaScript异步编程基础

前言:

这段时间在学习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设计者就将所有任务分以下两种任务以解决这个问题:

  1. 同步任务(在主线程中,只有前面的代码执行完毕后,后面的才能执行)
  2. 异步任务(从主线程提出来,异步执行,当执行完毕后在任务队列中放入一个事件,等主线程的任务执行完毕后再从任务队列中读取该任务的事件,并执行该任务)

主线程与任务队列的示意图如下(转自阮一峰老师的JavaScript 运行机制详解:再谈Event Loop):

image

事件循环(Event Loop)

前面我们提到,当JavaScript在异步任务完成后会通知主线程该任务可以执行了,那么又是如何 通知的呢?其实用一句话就可以去描述这个过程:

工作线程将信息放到任务队列中,主线程则通事件循环过程去读取完成的消息。

让我们再来看这段代码:

console.log(1);
setTimeout(function() {
    console.log(2);
},1000);
console.log(3);

// 1 3 2

其实它会经历一下几个步骤:

  1. 打印 1
  2. 调用 setTimeout,发现是一个异步任务,从主线程中提出,进行异步运行。
  3. 打印 3
  4. 异步任务运行完毕,工作线程在任务队列中放置一个事件。
  5. 主线程所有同步事件执行完毕后,通过事件循环读取事件,然后将异步任务放入主线程最后端执行。
  6. 打印 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,这需要在实践时额外注意,以避免出现阻塞的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,762评论 1 17
  • 本文首发于我的个人博客:「程序员充电站」[https://itcharge.cn]文章链接:「传送门」[https...
    ITCharge阅读 347,973评论 308 1,926
  • 文/孤鸟差鱼 我不想和过去有太多牵扯 可我甩不掉过去的烙印 躲躲藏藏后的大大方方 最后还是没结果 难为了这一段历程...
    孤鸟差鱼阅读 131评论 0 1
  • 我喜欢画画 画自己想画的 最开始画的时候忘记拍照了,画到一半才想起... 画完这幅画之后自己特别喜欢,把画都拍下来...
    Cherry爱吃樱桃阅读 93评论 2 1