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