回調地獄以及解決方法


前言

在正式了解“回調地獄”之前,我們先了解兩個概念:

回調函數
當一個函數作為參數傳入另一個參數中,並且它不會立即執行,只有當滿足一定條件后該函數才可以執行,這種函數就稱為回調函數。我們熟悉的定時器和Ajax中就存在有回調函數:

setTimeout(function(){   
    //function(){console.log('執行了回調函數')}就是回調函數,它只有在3秒后才會執行
	console.log('執行了回調函數');
},3000)  //3000毫秒

這里的回調函數是function(){console.log('執行了回調函數')},在滿足時間3秒后執行。

這里的回調函數是function(){console.log('執行了回調函數')},在滿足時間3秒后執行。

			//1.創建異步對象
			var xhr=new XMLHttpRequest();
            //2.綁定監聽事件(接收請求)
			xhr.onreadystatechange=function(){
				//此方法會被調用4次
				//最后一次,readyState==4
				//並且響應狀態碼為200時,才是我們要的響應結果 xhr.status==200
				if(xhr.readyState==4 && xhr.status==200){
					//把響應數據存儲到變量result中
					var result=xhr.responseText;
					console.log(result);
				}
			}
      //3.打開鏈接(創建請求)
			xhr.open("get","/demo/ajaxDemo",true);
      //4.發送請求
			xhr.send();

這里的回調函數是xhr.onreadystatechange綁定的函數,在xhr.send()發送請求並拿到響應后執行。

異步任務
與之相對應的概念是“同步任務”,同步任務在主線程上排隊執行,只有前一個任務執行完畢,才能執行下一個任務。異步任務不進入主線程,而是進入異步隊列,前一個任務是否執行完畢不影響下一個任務的執行。同樣,還拿定時器作為異步任務舉例:

 setTimeout(function(){
            console.log('執行了回調函數');
        },3000)
        console.log('111');

如果按照代碼編寫的順序,應該先輸出“執行了回調函數”,再輸出“111”。但實際輸出為:

這種不阻塞后面任務執行的任務就叫做異步任務。
接下來讓我們看看什么是回調地獄。

一、什么是回調地獄(就是為了在異步函數當中順序執行代碼而不斷嵌套調用回調函數)

比如我要說一句話,語序必須是下面這樣的:武林要以和為貴,要講武德,不要搞窩里斗。
我必須要這樣操作,才能保證順序正確:

    setTimeout(function () {  //第一層
        console.log('武林要以和為貴');
        setTimeout(function () {  //第二程
            console.log('要講武德');
            setTimeout(function () {   //第三層
                console.log('不要搞窩里斗');
            }, 1000)
        }, 2000)
    }, 3000)

可以看到,代碼中的回調函數套回調函數,居然套了3層,這種回調函數中嵌套回調函數的情況就叫做回調地獄。

總結一下,回調地獄就是為是實現代碼順序執行而出現的一種操作,它會造成我們的代碼可讀性非常差,后期不好維護。

那該如何解決回調地獄呢?

二、如何解決回調地獄

1.Promise

Promise是js中的一個原生對象,是一種異步編程的解決方案,可以替換掉傳統的回調函數解決方案。

Promise構造函數接收一個函數作為參數,我們需要處理的異步任務就卸載該函數體內,該函數的兩個參數是resolve,reject。異步任務執行成功時調用resolve函數返回結果,反之調用reject。
Promise對象的then方法用來接收處理成功時響應的數據,catch方法用來接收處理失敗時相應的數據。
Promise的鏈式編程可以保證代碼的執行順序,前提是每一次在than做完處理后,一定要return一個Promise對象,這樣才能在下一次than時接收到數據。
下面是實例代碼:

  function fn(str){
            var p=new Promise(function(resolve,reject){
                //處理異步任務
                var flag=true;
                setTimeout(function(){
                    if(flag){
                        resolve(str)
                    }
                    else{
                        reject('操作失敗')
                    }
                })
            })
            return p;
        }
fn('武林要以和為貴')
    .then((data)=>{
        console.log(data);
        return fn('要講武德');
    })
    .then((data)=>{
        console.log(data);
        return fn('不要搞窩里斗')
    })
    .then((data)=>{
        console.log(data);
    })
    .catch((data)=>{
        console.log(data);
    })

但是Promise最大的問題就是代碼冗余,原來的異步任務被Promise封裝一下,不管什么操作都用than,就會導致一眼看過去全是than…than…than…,這樣也是不利於代碼維護的。

所以下面的async/await 可以時代碼看起來更像同步代碼。

2.async/await

首先我們看async關鍵字,他作為一個關鍵字放到聲明函數前面,表示該函數為一個異步任務,不會阻塞后面函數的執行

async function fn(){
            return '不講武德';
}
        console.log(fn());

可以看到async函數返回數據時自動封裝為一個Promise對象。

和Promise對象一樣,處理異步任務時也可以按照成功和失敗來返回不同的數據,處理成功時用than方法來接收,失敗時用catch方法來接收數據:

    async function fn() {
        var flag = true;
        if (flag) {
            return '不講武德';
        }
        else{
            throw '處理失敗'
        }
    }
    fn()
    .then(data=>{
        console.log(data);
    })
    .catch(data=>{
        console.log(data);
    })

    console.log('先執行我,表明async聲明的函數是異步的');

當把flag設置為false是,執行結果為:

async關鍵字說完了,我們看看awai關鍵字

await關鍵字只能在使用async定義的函數中使用
await后面可以直接跟一個 Promise實例對象(可以跟任何表達式,更多的是跟一個返回Promise對象的表達式)
await函數不能單獨使用
await可以直接拿到Promise中resolve中的數據。

  //封裝一個返回promise的異步任務
        function fn(str) {
            var p = new Promise(function (resolve, reject) {
                var flag = true;
                setTimeout(function () {
                    if (flag) {
                        resolve(str)
                    } else {
                        reject('處理失敗')
                    }
                })
            })
            return p;
        }
//封裝一個執行上述異步任務的async函數
    async function test(){
        var res1=await fn('武林要以和為貴');  //await直接拿到fn()返回的promise的數據,並且賦值給res
        var res2=await fn('要講武德');
        var res3=await fn('不要搞窩里斗');
        console.log(res1,res2,res3);
    }
    //執行函數
    test();

為什么叫await等待呢,因為當代碼執行到async函數中的await時,代碼就在此處等待不繼續往下執行,知道await拿到Promise對象中resolve的數據,才繼續往下執行,這樣就保證了代碼的執行順序,而且使異步代碼看起來更像同步代碼。

總結

總結一下,當我們寫代碼遇到異步回調時,我們想讓異步代碼按照我們想要的順序執行,如果按照傳統的嵌套方式,就會出現回調地獄,這樣的代碼不利於維護,我們可以通過Promise對象進行鏈式編程來解決,這樣盡管可以解決問題,但是ES7給我們提供了更加舒適的async/await語法糖,可以使得異步代碼看起來更像是同步代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM