1. 程式人生 > >JavaScript異步編程

JavaScript異步編程

completed 瀏覽器渲染 settime 也看 ready ott err per 分享

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實現了自動化

寫的不周到或者有錯誤的地方歡迎各位大神及時指出。

歡迎糾錯~

JavaScript異步編程