平時開發常常會用到js異步編程,因爲前端展現頁面都是基於網絡機頂盒(IPTV的通常性能不太好,OTT較好),目前公司主要採起的異步編程的方式有setTimeout、setInterval、requestAnimationFrame、ajax,爲何會用到異步呢,就拿業務來講,若前端所有采起同步的方式,那加載圖片、生成dom、網絡數據請求都會大大增長頁面渲染時長。前端
JS 是單線程運行的,這意味着兩段代碼不能同時運行,而是必須逐步地運行,因此在同步代碼執行過程當中,異步代碼是不執行的。只有等同步代碼執行結束後,異步代碼纔會被添加到事件隊列中。node
這裏就涉及到執行棧和任務隊列:web
同步代碼是依次存放在執行棧中,遵循LIFO原則;ajax
異步代碼存放在任務隊列中,任務隊列又分宏任務和微任務(微任務執行優先級高於宏任務),遵循FIFO原則;chrome
請看下面代碼執行的順序(能夠先思考一下看看與正確輸出順序是否一致)編程
function foo(){
console.log('start...');
return bar();
}
function bar(){
console.log('bar...');
}
//這裏採用ES6的箭頭函數、Promise函數
var promise = new Promise(function(resolve,reject){
console.log('promise...');
resolve();
});
promise.then(()=>console.log('promise resolve...'));
setTimeout(()=>console.log('timeout...'),0);
foo()
console.log('end...');
複製代碼
請看答案
json
promise...
start...
bar...
end...
promise resolve...
timeout...數組
這裏分析一下(你們不要糾結任務隊列的叫法,本人說明的異步微任務、異步宏任務暫無根據,理解便可,請勿深究):promise
程序正式開始執行是從9行初始化promise對象開始,首先打印promise...瀏覽器
而後往下執行發現是promise.then回調函數,此爲異步微任務,放入任務隊列中,等待同步任務執行完才能執行
再往下執行是timeout定時器,此爲異步宏任務,也放入任務隊列中,等待同步任務執行完、異步微任務才能執行
再往下是foo方法,此爲同步任務,借用網絡流行的一句話 「JavaScript中的函數是一等公民」,打印日誌start...後回調執行bar方法,到這裏就有兩個執行棧了(依次將foo、bar放入棧中,bar執行完就彈出棧,foo依次彈出)
關於併發模型和Event Loop 請看MDN
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
複製代碼
關於異步編程的方式,經常使用的定時器、ajax、Promise、Generator、async/await,詳細介紹以下:
3.1.1.setTimeout與setInterval
這裏拿setTimeout來舉例
簡單的時鐘
(function(){
var div = document.createElement('div'),timer;
document.body.appendChild(div);
//同步代碼,5s後執行異步代碼塊顯示時鐘
//doSomething()
setTimeout(function(){
execFn();
},5000);
function timeChange(callback){
div.innerHTML = '當前時間:'+getCurrentTime();
if(new Date().getSeconds() %5 === 0){
//當前秒數是5的倍數關閉定時器
clearTimeout(timer);
//doSomething...
console.log(timer);
timer = setTimeout(execFn,100);
}else{
clearTimeout(timer);
execFn();
}
}
function execFn(){
timer1 = window.setTimeout(timeChange,1000);
}
function getCurrentTime(){
var d = new Date();
return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes()+':'+d.getSeconds()+' 星期'+getWeek();
}
function getWeek(){
var d = new Date();
var week;
switch(d.getDay()){
case(4):week='四';break;
//省略
default:week='*';break;
}
return week;
}
})();
複製代碼
正常的邏輯代碼確定要複雜的多,可是利用setTimeou編寫異步代碼的邏輯大體上是這麼處理的。
看下面的例子
你們是否有疑問,爲啥不是先輸出2再輸出1
setTimeout與setInterval執行的間隔時間爲4~5ms
下面看setInterval代碼
計數count輸出爲252,因此執行的間隔時間約爲4ms
看看caniuser支持的狀況
看這趨勢除了opera外其餘瀏覽器之後都支持requestAnimationFrame方法
平時業務中也看到公司同事封裝了requestAnimationFrame方法。若是碰到某些版本的瀏覽器不支持此方法,則須要重寫,requestAnimationFrame其實與防抖節流實現的原理有些類似,請看代碼
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
}
if(!window.requestAnimationFrame){
var lastTime = 0;
window.requestAnimationFrame = function(callback){
var now = new Date().getTime();
var nextTime = Math.max(lastTime + 16, now);//瀏覽器渲染的間隔時間大約16ms
return window.setTimeout(function(){
lastTime = nextTime;
callback();
},nextTime - now);
};
}
複製代碼
有興趣的同窗能夠看看這位大神的傑做
https://codepen.io/caryforchristine/pen/oMQMQz
直接看一個簡單的ajax異步處理代碼
(function(){
var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
var url = "authorInfo.json";
xmlhttp.onreadystatechange = function(){
if(xmlhttp.readyState==4){
if(xmlhttp.status==200){
console.log(xmlhttp.response);
//異步獲取數據後再doSomething
}
}
}
xmlhttp.open('GET',url,true);
xmlhttp.send(null);
})();
複製代碼
chrome打印日誌
Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象
簡單的讀取文件實例
var fs = require('fs')
var read = function (filename){
var promise = new Promise(function(resolve, reject){
fs.readFile(filename, 'utf8', function(err, data){
if (err){
reject(err);
}
resolve(data);
})
});
return promise;
}
read('authorInfo.json')
.then(function(data){
console.log(data);
return read('not_exist_file');
})
.then(function(data){
console.log(data);
})
.catch(function(err){
console.log("error caught: " + err);
})
.then(function(data){
console.log("completed");
})
複製代碼
用node運行結果以下:
Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject(函數)
當狀態由pending變成resolved執行resolve(),變成rejected則執行reject(),當promise實例生成時能夠用then指定回調
then(function success(){},function fail(){}),此方法仍是會返回一個新的promise對象,因此能夠進行鏈式調用
有關Promise包括下文要提到的Generator請看阮老師博客
本人在第一次接觸Generator的時候以爲特神奇,畢竟以前歷來沒有想過函數會斷點執行(在下描述不許確,勿噴),也就是說函數執行一部分能夠停下來處理另外的代碼塊,而後再回到暫停處繼續執行。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。
因而可知Generator返回的是一個遍歷器對象,能夠用for of(ES6新特性,主要是針對具備Symbol.iterator屬性的對象,包括數組,set,map,類數組等等)進行遍歷,
Generator語法 function* name(){},通常*和函數名中間有個空格,函數體內可經過yield關鍵字修飾,須要注意的是,yield後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。你們是否會以爲Generator要手動執行next方法過於麻煩呢,接下來介紹當前js對異步的終極解決方案
async和await是ES 7中的新語法,新到連ES 6都不支持。
能夠利用babel轉換
在線轉換地址:https://babeljs.io/ ,也能夠本身安裝babel-cli進行轉換
const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
try {
const file1 = await readFile('zh_cn.json');
const file2 = await readFile('authorInfo.json');
console.log(file1.toString(),file2.toString());
} catch (e) {
console.log('出錯啦');
}
}
readJsonFile();
複製代碼
能夠看到異步依次讀取兩個文件,若是利用Generator的話須要手動執行next,async/await實現了自動化
寫的不周到或者有錯誤的地方歡迎各位大神及時指出,轉載請註明出處,謝謝!
歡迎糾錯~