深入理解 JavaScript 非同步系列(2)—— jquery的解決方案

王福朋發表於2017-03-08

第一部分,jQuery-1.5 之後的 ajax

本地址 http://www.cnblogs.com/wangfupeng1988/p/6515779.html 未經允許不得轉載~

$.ajax這個函式各位應該都比較熟悉了,要完整的講解 js 的非同步操作,就必須先從$.ajax這個方法說起。

想要學到全面的知識,大家就不要著急,跟隨我的節奏來,並且相信我。我安排的內容,肯定都是有用的,對主題無用的東西,我不會拿來佔用大家的時間。

本節內容概述

  • 傳統的$.ajax
  • 1.5 版本之後的$.ajax
  • 改進之後的好處
  • 和後來的Promise的關係
  • 如何實現的?

傳統的$.ajax

先來一段最常見的$.ajax的程式碼,當然是使用萬惡的callback方式

var ajax = $.ajax({
    url: 'data.json',
    success: function () {
        console.log('success')
    },
    error: function () {
        console.log('error')
    }
})

console.log(ajax) // 返回一個 XHR 物件

至於這麼做會產生什麼樣子的詬病,我想大家應該都很明白了。不明白的自己私下去查,但是你也可以繼續往下看,你只需要記住這樣做很不好就是了,要不然 jquery 也不會再後面進行改進

1.5 版本之後的$.ajax

但是從v1.5開始,以上程式碼就可以這樣寫了:可以鏈式的執行done或者fail方法

var ajax = $.ajax('data.json')
ajax.done(function () {
        console.log('success 1')
    })
    .fail(function () {
        console.log('error')
    })
    .done(function () {
         console.log('success 2')
    })

console.log(ajax) // 返回一個 deferred 物件

大家注意看以上兩段程式碼中都有一個console.log(ajax),但是返回值是完全不一樣的。

  • v1.5之前,返回的是一個XHR物件,這個物件不可能有done或者fail的方法的
  • v1.5開始,返回一個deferred物件,這個物件就帶有donefail的方法,並且是等著請求返回之後再去呼叫

改進之後的好處

這是一個標誌性的改造,不管這個概念是誰最先提出的,它在 jquery 中首先大量使用並讓全球開發者都知道原來 ajax 請求還可以這樣寫。這為以後的Promise標準制定提供了很大意義的參考,你可以以為這就是後面Promise的原型。

記住一句話————雖然 JS 是非同步執行的語言,但是人的思維是同步的————因此,開發者總是在尋求如何使用邏輯上看似同步的程式碼來完成 JS 的非同步請求。而 jquery 的這一次更新,讓開發者在一定程度上得到了這樣的好處。

之前無論是什麼操作,我都需要一股腦寫到callback中,現在不用了。現在成功了就寫到done中,失敗了就寫到fail中,如果成功了有多個步驟的操作,那我就寫很多個done,然後鏈式連線起來就 OK 了。

和後來的Promise的關係

以上的這段程式碼,我們還可以這樣寫。即不用donefail函式,而是用then函式。then函式的第一個引數是成功之後執行的函式(即之前的done),第二個引數是失敗之後執行的函式(即之前的fail)。而且then函式還可以鏈式連線。

var ajax = $.ajax('data.json')
ajax.then(function () {
        console.log('success 1')
    }, function () {
        console.log('error 1')
    })
    .then(function () {
        console.log('success 2')
    }, function () {
        console.log('error 2')
    })

如果你對現在 ES6 的Promise有了解,應該能看出其中的相似之處。不瞭解也沒關係,你只需要知道它已經和Promise比較接近了。後面馬上會去講Promise

如何實現的?

明眼人都知道,jquery 不可能改變非同步操作需要callback的本質,它只不過是自己定義了一些特殊的 API,並對非同步操作的callback進行了封裝而已。

那麼 jquery 是如何實現這一步的呢?請聽下回分解!

 

第二部分,jQuery deferred

上一節講到 jquery v1.5 版本開始,$.ajax可以使用類似當前Promisethen函式以及鏈式操作。那麼它到底是如何實現的呢?在此之前所用到的callback在這其中又起到了什麼作用?本節給出答案

本節使用的程式碼參見這裡

本節內容概述

  • 寫一個傳統的非同步操作
  • 使用$.Deferred封裝
  • 應用then方法
  • 有什麼問題?

寫一個傳統的非同步操作

給出一段非常簡單的非同步操作程式碼,使用setTimeout函式。

var wait = function () {
    var task = function () {
        console.log('執行完成')
    }
    setTimeout(task, 2000)
}
wait()

以上這些程式碼執行的結果大家應該都比較明確了,即 2s 之後列印出執行完成但是我如果再加一個需求 ———— 要在執行完成之後進行某些特別複雜的操作,程式碼可能會很多,而且分好幾個步驟 ———— 那該怎麼辦? 大家思考一下!

如果你不看下面的內容,而且目前還沒有Promise的這個思維,那估計你會說:直接在task函式中寫就是了!不過相信你看完下面的內容之後,會放棄你現在的想法。

使用$.Deferred封裝

好,接下來我們讓剛才簡單的幾行程式碼變得更加複雜。為何要變得更加複雜?是因為讓以後更加複雜的地方變得簡單。這裡我們使用了 jquery 的$.Deferred,至於這個是個什麼鬼,大家先不用關心,只需要知道$.Deferred()會返回一個deferred物件,先看程式碼,deferred物件的作用我們會面會說。

function waitHandle() {
    var dtd = $.Deferred()  // 建立一個 deferred 物件

    var wait = function (dtd) {  // 要求傳入一個 deferred 物件
        var task = function () {
            console.log('執行完成')
            dtd.resolve()  // 表示非同步任務已經完成
        }
        setTimeout(task, 2000)
        return dtd  // 要求返回 deferred 物件
    }

    // 注意,這裡一定要有返回值
    return wait(dtd)
}

以上程式碼中,又使用一個waitHandle方法對wait方法進行再次的封裝。waitHandle內部程式碼,我們分步驟來分析。跟著我的節奏慢慢來,保證你不會亂。

  • 使用var dtd = $.Deferred()建立deferred物件。通過上一節我們知道,一個deferred物件會有done failthen方法(不明白的去看上一節)
  • 重新定義wait函式,但是:第一,要傳入一個deferred物件(dtd引數);第二,當task函式(即callback)執行完成之後,要執行dtd.resolve()告訴傳入的deferred物件,革命已經成功。第三;將這個deferred物件返回。
  • 返回wait(dtd)的執行結果。因為wait函式中返回的是一個deferred物件(dtd引數),因此wait(dtd)返回的就是dtd————如果你感覺這裡很亂,沒關係,慢慢捋,一行一行看,相信兩三分鐘就能捋順!

最後總結一下,waitHandle函式最終return wait(dtd)即最終返回dtd(一個deferred)物件。針對一個deferred物件,它有done failthen方法(上一節說過),它還有resolve()方法(其實和resolve相對的還有一個reject方法,後面會提到)

應用then方法

接著上面的程式碼繼續寫

var w = waitHandle()
w.then(function () {
    console.log('ok 1')
}, function () {
    console.log('err 1')
}).then(function () {
    console.log('ok 2')
}, function () {
    console.log('err 2')
})

上面已經說過,waitHandle函式最終返回一個deferred物件,而deferred物件具有done fail then方法,現在我們正在使用的是then方法。至於then方法的作用,我們上一節已經講過了,不明白的同學抓緊回去補課。

執行這段程式碼,我們列印出來以下結果。可以將結果對標以下程式碼時哪一行。

執行完成
ok 1
ok 2

此時,你再回頭想想我剛才說提出的需求(要在執行完成之後進行某些特別複雜的操作,程式碼可能會很多,而且分好幾個步驟),是不是有更好的解決方案了?

有同學肯定發現了,程式碼中console.log('err 1')console.log('err 2')什麼時候會執行呢 ———— 你自己把waitHandle函式中的dtd.resolve()改成dtd.reject()試一下就知道了。

  • dtd.resolve() 表示革命已經成功,會觸發then中第一個引數(函式)的執行,
  • dtd.reject() 表示革命失敗了,會觸發then中第二個引數(函式)執行

有什麼問題?

總結一下一個deferred物件具有的函式屬性,並分為兩組:

  • dtd.resolve dtd.reject
  • dtd.then dtd.done dtd.fail

我為何要分成兩組 ———— 這兩組函式,從設計到執行之後的效果是完全不一樣的。第一組是主動觸發用來改變狀態(成功或者失敗),第二組是狀態變化之後才會觸發的監聽函式。

既然是完全不同的兩組函式,就應該徹底的分開,否則很容易出現問題。例如,你在剛才執行程式碼的最後加上這麼一行試試。

w.reject()

那麼如何解決這一個問題?請聽下回分解!

 

第三部分,jQuery promise

上一節通過一些程式碼演示,知道了 jquery 的deferred物件是解決了非同步中callback函式的問題,但是

本節使用的程式碼參見這裡

本節內容概述

  • 返回promise
  • 返回promise的好處
  • promise 的概念

返回promise

我們對上一節的的程式碼做一點小小的改動,只改動了一行,下面註釋。

function waitHandle() {
    var dtd = $.Deferred()
    var wait = function (dtd) {
        var task = function () {
            console.log('執行完成')
            dtd.resolve()
        }
        setTimeout(task, 2000)
        return dtd.promise()  // 注意,這裡返回的是 primise 而不是直接返回 deferred 物件
    }
    return wait(dtd)
}

var w = waitHandle() // 經過上面的改動,w 接收的就是一個 promise 物件
$.when(w)
 .then(function () {
    console.log('ok 1')
 })
 .then(function () {
    console.log('ok 2')
 })

改動的一行在這裡return dtd.promise(),之前是return dtddtd是一個deferred物件,而dtd.promise就是一個promise物件。

promise物件和deferred物件最重要的區別,記住了————promise物件相比於deferred物件,缺少了.resolve.reject這倆函式屬性。這麼一來,可就完全不一樣了。

上一節我們提到一個問題,就是在程式的最後一行加一句w.reject()會導致亂套,你現在再在最後一行加w.reject()試試 ———— 保證亂套不了 ———— 而是你的程式不能執行,直接報錯。因為,wpromise物件,不具備.reject屬性。

返回promise的好處

上一節提到deferred物件有兩組屬性函式,而且提到應該把這兩組徹底分開。現在通過上面一行程式碼的改動,就分開了。

  • waitHandle函式內部,使用dtd.resolve()來該表狀態,做主動的修改操作
  • waitHandle最終返回promise物件,只能去被動監聽變化(then函式),而不能去主動修改操作

一個“主動”一個“被動”,完全分開了。

promise 的概念

jquery v1.5 版本釋出時間距離現在(2017年初春)已經老早之前了,那會兒大家網頁標配都是 jquery 。無論裡面的deferredpromise這個概念和想法最早是哪位提出來的,但是最早展示給全世界開發者的是 jquery ,這算是Promise這一概念最先的提出者。

其實本次課程主要是給大家分析 ES6 的Promise Generatorasync-await,但是為何要從 jquery 開始(大家現在用 jquery 越來越少)?就是要給大家展示一下這段歷史的一些起點和發展的知識。有了這些基礎,你再去接受最新的概念會非常容易,因為所有的東西都是從最初順其自然發展進化而來的,我們要去用一個發展進化的眼光學習知識,而不是死記硬背。

求打賞

如果你看完了,感覺還不錯,歡迎給我打賞 ———— 以激勵我更多輸出優質內容

最後,github地址是 https://github.com/wangfupeng1988/js-async-tutorial 歡迎 star 和 pr

-------

學習作者教程:《前端JS高階面試》《前端JS基礎面試題》《React.js模擬大眾點評webapp》《zepto設計與原始碼分析》《json2.js原始碼解讀

相關文章