JavaScript 异步机制及应用 入门教程
1. 异步与同步 技术研究
(1). 概念介绍
异步: asynchronous 简写async
同步: synchronous 简写sync
用比方来比喻
异步就是: N个人同时起跑, 起点和出发时间相同, 在起跑时不去关心其他人会啥时候跑完~尼玛这不废话吗?大家都才起跑怎么知道别人多就跑完.
同步就是: N个人接力跑, 起点和出发时间不同, 且后一个人会等待前一个人跑完才能继续跑, 也就是要关心前一个人的结果(上一行代码的返回值).
(2). JS里面的异步/同步
JS运行场景多数是在用户浏览器上, 程序效率优劣会直接影响用户的体验交互. 比如一个网站, 在用户注册时, 会ajax校验输入再发提交表单, 如果用同步就可能会一直卡着等待ajax响应, 好几秒结束后再跳到注册结果页, 这个体验将是非常糟糕的.
说到JS的异步, 不得不提及一个非常有代表意义函数了.
JavaScript
var 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并不会理会 onreadystatechang
e方法, 而当改送请求到达服务器, 开始响应或者响应状态改变时会调用 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
JavaScript
for( 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
JavaScript
for( 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
JavaScript
for( 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 }
有可能你会感到困惑, 为何可以给ajaxObj设置一个i属性然后在回调时用第三个回调参数的i属性呢?
jQuery.ajax文档中写到:
jQuery.ajax( [settings ] )
settings
...
success: Function( Anything data, String textStatus, jqXHR jqXHR )
第1个参数就是响应的文本/HTML/XML/数据/json之类的, 跟你的dataType设置有关
第2个参数就是status状态, 如success
第3个参数就是jqXHR, 也就是jQuery的XMLHttpRequest对象, 当然, 在这里就是$.ajax()生成的对象, 也就是事件的触发者本身,
给本身设置一个属性(ajaxObj.i = i), 然后再调用本身的回调时, 使用本身的那个属性(obj.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