JavaScript 异步机制及应用 入门教程

jopen 9年前

原文 http://hao.jser.com/archive/7997/

1. 异步与同步 技术研究

(1). 概念介绍

异步: asynchronous 简写async同步: synchronous 简写sync

用比方来比喻

异步就是: N个人同时起跑, 起点和出发时间相同, 在起跑时不去关心其他人会啥时候跑完~尼玛这不废话吗?大家都才起跑怎么知道别人多就跑完.

同步就是: N个人接力跑, 起点和出发时间不同, 且后一个任务会等待前一个人跑完才能继续跑, 也就是要关心前一个人的结果(上一行代码的返回值).

(2). JS里面的异步/同步

JS运行场景多数是在用户浏览器上, 程序效率优劣会直接影响用户的体验交互. 比如一个网站, 在用户注册时, 会ajax校验输入再发提交表单, 如果用同步就可能会一直卡着等待ajax响应, 好几秒结束后再跳到注册结果页, 这个体验将是非常糟糕的.

说到JS的异步, 不得不提及一个非常有代表意义函数了.

JavaScriptvar url = '/action/';  var data = 'i=1';  xmlHTTP = new XMLHttpRequest();  xmlHTTP.nonce = nonce;  xmlHTTP.open("POST", url);  xmlHTTP.onreadystatechange = function(a) {   if(a.target.readyState!=4)return false;   try{    console.log(a.target.responseText)   }catch(e){    return false;   }  };  xmlHTTP.send(data);

或者在jQuery写作:

JavaScript$.ajax({   url: '/action/',   type: 'POST',   data: 'i=1',   success: function(responseText){    console.log(responseText);   }  })

上面的无论是xmlHTTP.onreadystatechange, 还是success, 在JavaScript中均称为回调方法,

以原生JS的XMLHttpRequest为例,xmlHTTP变量是个XMLHttpRequest对象, 他的onreadystatechange是在每次请求响应状态发生变化时会触发的一个函数/方法, 然后在发出请求xmlHTTP.send(data)的时候, JS并不会理会onreadystatechange方法, 而当改送请求到达服务器, 开始响应或者响应状态改变时会调用onreadystatechange方法:

也就是

1) 请求发出

2) 服务器开始响应数据

3) 执行回调方法, 可能执行多次

以jQuery版为例, $.ajax本身是个函数, 唯一一个参数是{…} 这个对象, 然后回调方法success是作为这个对象的一个属性传入$.ajax的.

$.ajax()先将数据post到’/action/’, 返回结果后再调用success(如果发生错误会调用error).

也就是

1) 请求发出   2) 服务器开始响应数据   3) 响应结束执行回调方法

然后作为函数$.ajax, 是函数就应该有返回值(哪怕没有return也会返回undefined), 他本身的返回值是多少呢?

分为async:true和async:false两个版本:

async:true版本:

JavaScript$.ajax({'url':'a.html', type:'GET', async:true})  > Object {readyState: 1}

async:false版本:

JavaScript$.ajax({'url':'robots.txt', type:'GET', false})  > Object {readyState: 4, responseText: "<!DOCTYPE HTML PUBLIC ...", status: 200, statusText: "OK"}

我们可以直接看到, async:true异步模式下, jquery/javascript未将结果返回… 而async:false就将结果返回了.

然后问题就来了, 为什么async:true未返回结果呢?

答案很简单:

因为在返回的时候, 程序不可能知道结果. 异步就是指不用等此操作执行出结果再往下执行, 也就是返回的值中未包含结果.

留下一个问题, 我们是不是为了程序流程的简单化而使用同步呢?

(3). 异步的困惑

先帖一段代码:a.php

php<?php  sleep(1);      // 休息一秒钟  echo '{}';

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){   $.ajax({   url: 'a.php',   type: 'POST',   dataType: 'json',   data: {data: i},   async: true,   // 默认即为异步   success: function(json) {     console.log(i + ': ' + json); // 打印    }   });  }

你们猜猜打印的那行会最终打印出什么内容?

1: {}  2: {}  3: {}  4: {}

吗?

错!

输出的将是:

4: {}  4: {}  4: {}  4: {}

你TM在逗我?

没有, 这并不是JS的BUG, 也不是jQuery的BUG.

这是因为, PHP休息了一秒, 而js异步地循环从1到4, 远远用不到1秒.

然后在1秒钟后, 才开始返回数据, 触发success, 此时此刻i已经自增成了4.

自然而然地, 第一次console.log(i...)就是4, 第二次也是, 第三次也是, 第四次也是.

那么如果我们希望程序输出也1,2,3,4这样输出怎么办呢?

两种方案:

1) 让后端输出i

a.php

php<?php  sleep(1);  echo '{i: ' . $_POST['data'] . '}'; // 这一行改了

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){   $.ajax({   url: 'a.php',   type: 'POST',   dataType: 'json',   data: {data: i},   async: true,   success: function(json) {     console.log(json.i + ': ' + json); // 这一行改了    }   });  }

2) 给回调的事件对象赋属性

a.php

php保持原代码不变

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){   ajaxObj = $.ajax({    // 将ajax赋给ajaxObj    url: 'a.php',    type: 'POST',    dataType: 'json',    data: {data: i},    async: true,    success: function(json, status, obj) { // 增加回调参数, jQuery文档有说第三个参数就是ajax方法产生的对象.      console.log(obj.i + ': ' + json); // 从jQuery.ajax返回的对象中取i    }   });   ajaxObj.i = i;   // 给ajaxObj赋属性i 值为循环的i   }

然后1)输出的结果将是

1: {i:1}  2: {i:2}  3: {i:3}  4: {i:4}

2)输出的结果将是

1: {}  2: {}  3: {}  4: {}

虽然略有区别, 但两者均可达到要求. 若要论代码的逼格, 相信你一定会被第二个方案给震惊的.

凭什么你给ajaxObj赋个属性就可以在success中用了呢?

请看(4). 异步的回调机制

(4). 异步的回调机制 —— 事件

一个有经验的JavaScript程序员一定会将js回调用得得心应手.

因为JavaScript天生异步, 异步的好处是顾及了用户的体验, 但坏处就是导致流程化循环或者递归的逻辑明明在别的语言中无任何问题, 却在js中无法取得期待的值…

而JavaScript异步在设计之初就将这一点考虑到了. 任何流行起来的JS插件方法, 如jQuery的插件, 一定考虑到了这一点了的.

举个例子.

ajaxfileupload插件, 实现原理是将选择的文件$.clone到一个form中, form的target设置成了一个页面中的iframe, 然后定时取iframe的contents().body, 即可获得响应的值.  如果要支持multiple文件上传(一些现代化的浏览器支持), 还是得要用`XMLHttpRequest`

如下面代码:

$('input#file').on('change', function(e){   for(i = 0; i < e.target.files.length; i++ ){    var data = new FormData();    data.append("file", e.target.files[i]);    xmlHTTP = new XMLHttpRequest();    xmlHTTP.open("POST", s.url);    xmlHTTP.onreadystatechange = function(a) { // a 为 事件event对象     if(a.target.readyState!=4)return false; // a.target为触发这个事件的对象 即xmlHTTP (XMLHttpRequest) 对象     try{      console.log(a.target.responseText);     }catch(e){      return false;     }    };    xmlHTTP.send(data);   }  })

你可以很明显地知道, 在onreadystatechange调用且走到console.log(a.target.responseText)时, 如果服务器不返回文件名, 我们根本并不知道返回的是哪个文件的URL. 如果根据i去取的话, 那么很容易地, 我们只会取到始终1个或几个, 并不能保证准确.

那么我们应该怎么去保证在console.log(a.target.responseText)时能知道我信上传的文件的基本信息呢?

$('input#file').on('change', function(e){   for(i = 0; i < e.target.files.length; i++ ){    var data = new FormData();    data.append("file", e.target.files[i]);    xmlHTTP = new XMLHttpRequest();    xmlHTTP.file = e.target.files[i];    xmlHTTP.open("POST", s.url);    xmlHTTP.onreadystatechange = function(a) {     if(a.target.readyState!=4)return false;     try{      console.log(a.target.file);   //这儿是上面`xmlHTTP.file = e.target.files[i]` 赋进去的      console.log(a.target.responseText);     }catch(e){      return false;     }    };    xmlHTTP.send(data);   }  })

是不是很简单?

2. 展望

(1). Google对同步JavaScript的态度

在你尝试在chrome打开的页面中执行async: false的代码时, chrome将会警告你:

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/.

(2). 职场展望

异步和事件将是JavaScript工程师必备技能

[完]Reference:

1.《Javascript异步编程的4种方法》     http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html  2.《什么是 Event Loop?》            http://www.ruanyifeng.com/blog/2013/10/event_loop.html  3.《JavaScript 运行机制详解:再谈Event Loop》 http://www.ruanyifeng.com/blog/2014/10/event-loop.html