JavaScript異步編程

1.前言

平時開發常常會用到js異步編程,因爲前端展現頁面都是基於網絡機頂盒(IPTV的通常性能不太好,OTT較好),目前公司主要採起的異步編程的方式有setTimeout、setInterval、requestAnimationFrame、ajax,爲何會用到異步呢,就拿業務來講,若前端所有采起同步的方式,那加載圖片、生成dom、網絡數據請求都會大大增長頁面渲染時長。前端

2.JS 運行機制

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
複製代碼

3.異步編程

關於異步編程的方式,經常使用的定時器、ajax、Promise、Generator、async/await,詳細介紹以下:

3.1.定時器

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

3.1.2.requestAnimationFrame

看看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

3.2.Ajax

直接看一個簡單的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打印日誌


3.3.Promise

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請看阮老師博客

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進行轉換

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

寫的不周到或者有錯誤的地方歡迎各位大神及時指出,轉載請註明出處,謝謝!

歡迎糾錯~

相關文章
相關標籤/搜索