JavaScript 異步機制及應用 入門教程
原文 http://hao.jser.com/archive/7997/
1. 異步與同步 技術研究
(1). 概念介紹
異步: asynchronous 簡寫async同步: synchronous 簡寫sync
用比方來比喻
異步就是: N個人同時起跑, 起點和出發時間相同, 在起跑時不去關心其他人會啥時候跑完~尼瑪這不廢話嗎?大家都才起跑怎么知道別人多就跑完.
同步就是: N個人接力跑, 起點和出發時間不同, 且后一個任務會等待前一個人跑完才能繼續跑, 也就是要關心前一個人的結果(上一行代碼的返回值).
</div>(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