JavaScript 異步編程的前世此生(上)

前言

提到 JavaScript 異步編程,不少小夥伴都很迷茫,本人花費大約一週的業餘時間來對 JS 異步作一個完整的總結,和各位同窗共勉共進步!html

目錄

part1 基礎部分node

  • 什麼是異步

part2 jQuery的解決方案jquery

  • jQuery-1.5 以後的 ajax
  • jQuery deferred
  • jQuery promise

part3 ES6-Promisegit

  • Promise 加入 ES6 標準
  • Promise 在 ES6 中的具體應用
  • 對標一下 Promise/A+ 規範
  • Promise 真的取代 callback 了嗎?
  • 用 Q.js 庫

part4 Generatores6

  • ES6 中的 Generator
  • Iterator 遍歷器
  • Generator 的具體應用
  • Thunk 函數
  • Generator 與異步操做
  • koa 中使用 Generator
  • Generator 的本質是什麼?是否取代了 callback

part5 async-awaitgithub

  • ES7 中引入 async-await
  • 如何在 nodejs v6.x版本中使用 async-await

part6 總結ajax

  • 總結

什麼是異步

提醒:若是你是初學 js 的同窗,還沒有有太多項目經驗和基礎知識,請就此打住,不要看這篇教程chrome

我思考問題、寫文章通常都不按討論出牌,別人寫過的東西我不會再照着抄一遍。所以,後面全部的內容,都是我看了許多資料以後,我的從新思考提煉總結出來的,這確定不能算是初級教程。npm

若是你是已有 js 開發經驗,並瞭解異步的基礎知識,到這裏來想深刻了解一下Promise Generatorasync-await,那就太好了,很是歡迎。編程

本節內容概述

  • JS 爲什麼會有異步
  • 異步的實現原理是什麼
  • 經常使用的異步操做有哪些

JS 爲什麼會有異步

首先記住一句話 —— JS 是單線程的語言,所謂「單線程」就是一根筋,對於拿到的程序,一行一行的執行,上面的執行爲完成,就傻傻的等着。例如

var i, t = Date.now() for (i = 0; i < 100000000; i++) { } console.log(Date.now() - t)  // 250 (chrome瀏覽器)

上面的程序花費 250ms 的時間執行完成,執行過程當中就會有卡頓,其餘的事兒就先撂一邊無論了。

執行程序這樣沒有問題,可是對於 JS 最初使用的環境 ———— 瀏覽器客戶端 ———— 就不同了。所以在瀏覽器端運行的 js ,可能會有大量的網絡請求,而一個網絡資源啥時候返回,這個時間是不可預估的。這種狀況也要傻傻的等着、卡頓着、啥都不作嗎?———— 那確定不行。

所以,JS 對於這種場景就設計了異步 ———— 即,發起一個網絡請求,就先無論這邊了,先幹其餘事兒,網絡請求啥時候返回結果,到時候再說。這樣就能保證一個網頁的流程運行。

異步的實現原理

先看一段比較常見的代碼

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

上面代碼中$.ajax()須要傳入兩個參數進去,urlsuccess,其中url是請求的路由,success是一個函數。這個函數傳遞過去不會當即執行,而是等着請求成功以後才能執行。對於這種傳遞過去不執行,等出來結果以後再執行的函數,叫作callback,即回調函數

再看一段更加能說明回調函數的 nodejs 代碼。和上面代碼基本同樣,惟一區別就是:上面代碼時網絡請求,而下面代碼時 IO 操做。

var fs = require('fs') fs.readFile('data1.json', (err, data) => { console.log(data.toString()) })

從上面兩個 demo 看來,實現異步的最核心原理,就是將callback做爲參數傳遞給異步執行函數,當有結果返回以後再觸發 callback執行,就是如此簡單!

經常使用的異步操做

開發中比較經常使用的異步操做有:

  • 網絡請求,如ajax http.get
  • IO 操做,如readFile readdir
  • 定時函數,如setTimeout setInterval

最後,請思考,事件綁定是否是也是異步操做?例如$btn.on('click', function() {...})。這個問題頗有意思,我會再後面的章節通過分析以後給出答案,各位先本身想一下。

 

jQuery-1.5 以後的 ajax

$.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 版本發佈時間距離如今(2018年)已經老早以前了,那會兒你們網頁標配都是 jquery 。不管裏面的deferredpromise這個概念和想法最先是哪位提出來的,可是最先展現給全世界開發者的是 jquery ,這算是Promise這一律念最早的提出者。

其實本次課程主要是給你們分析 ES6 的Promise Generatorasync-await,可是爲什麼要從 jquery 開始(你們如今用 jquery 愈來愈少)?就是要給你們展現一下這段歷史的一些起點和發展的知識。有了這些基礎,你再去接受最新的概念會很是容易,由於全部的東西都是從最初順其天然發展進化而來的,咱們要去用一個發展進化的眼光學習知識,而不是死記硬背。

Promise 加入 ES6 標準

從 jquery v1.5 發佈通過若干時間以後,Promise 終於出如今了 ES6 的標準中,而當下 ES6 也正在被大規模使用。

本節內容概述

  • 寫一段傳統的異步操做
  • Promise進行封裝

寫一段傳統的異步操做

仍是拿以前講 jquery deferred對象時的那段setTimeout程序

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

以前咱們使用 jquery 封裝的,接下來將使用 ES6 的Promise進行封裝,你們注意看有何不一樣。

Promise進行封裝

const wait =  function () { // 定義一個 promise 對象
    const promise = new Promise((resolve, reject) => { // 將以前的異步操做,包括到這個 new Promise 函數以內
        const task = function () { console.log('執行完成') resolve() // callback 中去執行 resolve 或者 reject
 } setTimeout(task, 2000) }) // 返回 promise 對象
    return promise }

注意看看程序中的註釋,那都是重點部分。從總體看來,感受此次比用 jquery 那次簡單一些,邏輯上也更加清晰一些。

  • 將以前的異步操做那幾行程序,用new Promise((resolve,reject) => {.....})包裝起來,最後return便可
  • 異步操做的內部,在callback中執行resolve()(代表成功了,失敗的話執行reject

接着上面的程序繼續往下寫。wait()返回的確定是一個promise對象,而promise對象有then屬性。

const w = wait() w.then(() => { console.log('ok 1') }, () => { console.log('err 1') }).then(() => { console.log('ok 2') }, () => { console.log('err 2') })

then仍是和以前同樣,接收兩個參數(函數),第一個在成功時(觸發resolve)執行,第二個在失敗時(觸發reject)時執行。並且,then還能夠進行鏈式操做。

以上就是 ES6 的Promise的基本使用演示。看完你可能會以爲,這跟以前講述 jquery 的不差很少嗎 ———— 對了,這就是我要在以前先講 jquery 的緣由,讓你感受一篇一篇看起來如絲般順滑!

接下來,將詳細說一下 ES6 Promise 的一些比較常見的用法,敬請期待吧!

Promise 在 ES6 中的具體應用

上一節對 ES6 的 Promise 有了一個最簡單的介紹,這一節詳細說一下 Promise 那些最多見的功能

本節課程概述

  • 準備工做
  • 參數傳遞
  • 異常捕獲
  • 串聯多個異步操做
  • Promise.allPromise.race的應用
  • Promise.resolve的應用
  • 其餘

準備工做

由於如下全部的代碼都會用到Promise,所以乾脆在全部介紹以前,先封裝一個Promise,封裝一次,爲下面屢次應用。

const fs = require('fs') const path = require('path')  // 後面獲取文件路徑時候會用到
const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) // 注意,這裏執行 reject 是傳遞了參數,後面會有地方接收到這個參數
            } else { resolve(data.toString()) // 注意,這裏執行 resolve 時傳遞了參數,後面會有地方接收到這個參數
 } }) }) }

以上代碼一個一段 nodejs 代碼,將讀取文件的函數fs.readFile封裝爲一個Promise。通過上一節的學習,我想你們確定都能看明白代碼的含義,要是看不明白,你就須要回爐重造了!

參數傳遞

咱們要使用上面封裝的readFilePromise讀取一個 json 文件../data/data2.json,這個文件內容很是簡單:{"a":100, "b":200}

先將文件內容打印出來,代碼以下。你們須要注意,readFilePromise函數中,執行resolve(data.toString())傳遞的參數內容,會被下面代碼中的data參數所接收到。

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) })

再加一個需求,在打印出文件內容以後,我還想看看a屬性的值,代碼以下。以前咱們已經知道then能夠執行鏈式操做,若是then有多步驟的操做,那麼前面步驟return的值會被當作參數傳遞給後面步驟的函數,以下面代碼中的a就接收到了return JSON.parse(data).a的值

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { // 第一步操做
 console.log(data) return JSON.parse(data).a  // 這裏將 a 屬性的值 return
}).then(a => { // 第二步操做
    console.log(a)  // 這裏能夠獲取上一步 return 過來的值
})

總結一下,這一段內容提到的「參數傳遞」其實有兩個方面:

  • 執行resolve傳遞的值,會被第一個then處理時接收到
  • 若是then有鏈式操做,前面步驟返回的值,會被後面的步驟獲取到

異常捕獲

咱們知道then會接收兩個參數(函數),第一個參數會在執行resolve以後觸發(還能傳遞參數),第二個參數會在執行reject以後觸發(其實也能夠傳遞參數,和resolve傳遞參數同樣),可是上面的例子中,咱們沒有用到then的第二個參數。這是爲什麼呢 ———— 由於不建議這麼用。

對於Promise中的異常處理,咱們建議用catch方法,而不是then的第二個參數。請看下面的代碼,以及註釋。

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) return JSON.parse(data).a }).then(a => { console.log(a) }).catch(err => { console.log(err.stack) // 這裏的 catch 就能捕獲 readFilePromise 中觸發的 reject ,並且能接收 reject 傳遞的參數
})

在若干個then串聯以後,咱們通常會在最後跟一個.catch來捕獲異常,並且執行reject時傳遞的參數也會在catch中獲取到。這樣作的好處是:

  • 讓程序看起來更加簡潔,是一個串聯的關係,沒有分支(若是用then的兩個參數,就會出現分支,影響閱讀)
  • 看起來更像是try - catch的樣子,更易理解

串聯多個異步操做

若是如今有一個需求:先讀取data2.json的內容,當成功以後,再去讀取data1.json。這樣的需求,若是用傳統的callback去實現,會變得很麻煩。並且,如今只是兩個文件,若是是十幾個文件這樣作,寫出來的代碼就無法看了(臭名昭著的callback-hell)。可是用剛剛學到的Promise就能夠輕鬆勝任這項工做

const fullFileName2 = path.resolve(__dirname, '../data/data2.json') const result2 = readFilePromise(fullFileName2) const fullFileName1 = path.resolve(__dirname, '../data/data1.json') const result1 = readFilePromise(fullFileName1) result2.then(data => { console.log('data2.json', data) return result1  // 此處只需返回讀取 data1.json 的 Promise 便可
}).then(data => { console.log('data1.json', data) // data 便可接收到 data1.json 的內容
})

上文「參數傳遞」提到過,若是then有鏈式操做,前面步驟返回的值,會被後面的步驟獲取到。可是,若是前面步驟返回值是一個Promise的話,狀況就不同了 ———— 若是前面返回的是Promise對象,後面的then將會被當作這個返回的Promise的第一個then來對待 ———— 若是你這句話看不懂,你須要將「參數傳遞」的示例代碼和這裏的示例代碼聯合起來對比着看,而後體會這句話的意思。

Promise.allPromise.race的應用

我還得繼續提出更加奇葩的需求,以演示Promise的各個經常使用功能。以下需求:

讀取兩個文件data1.jsondata2.json,如今我須要一塊兒讀取這兩個文件,等待它們所有都被讀取完,再作下一步的操做。此時須要用到Promise.all

// Promise.all 接收一個包含多個 promise 對象的數組
Promise.all([result1, result2]).then(datas => { // 接收到的 datas 是一個數組,依次包含了多個 promise 返回的內容
    console.log(datas[0]) console.log(datas[1]) })

讀取兩個文件data1.jsondata2.json,如今我須要一塊兒讀取這兩個文件,可是隻要有一個已經讀取了,就能夠進行下一步的操做。此時須要用到Promise.race

// Promise.race 接收一個包含多個 promise 對象的數組
Promise.race([result1, result2]).then(data => { // data 即最早執行完成的 promise 的返回值
 console.log(data) })

Promise.resolve的應用

從 jquery 引出,到此即將介紹完 ES6 的Promise,如今咱們再回歸到 jquery 。

你們都是到 jquery v1.5 以後$.ajax()返回的是一個deferred對象,而這個deferred對象和咱們如今正在學習的Promise對象已經很接近了,可是還不同。那麼 ———— deferred對象可否轉換成 ES6 的Promise對象來使用??

答案是能!須要使用Promise.resolve來實現這一功能,請看如下代碼:

// 在瀏覽器環境下運行,而非 node 環境
cosnt jsPromise = Promise.resolve($.ajax('/whatever.json')) jsPromise.then(data => { // ...
})

注意:這裏的Promise.resolve和文章最初readFilePromise函數內部的resolve函數可千萬不要混了,徹底是兩碼事兒。JS 基礎好的同窗一看就明白,而這裏看不明白的同窗,要特別注意。

實際上,並非Promise.resolve對 jquery 的deferred對象作了特殊處理,而是Promise.resolve可以將thenable對象轉換爲Promise對象。什麼是thenable對象?———— 看個例子

// 定義一個 thenable 對象
const thenable = { // 所謂 thenable 對象,就是具備 then 屬性,並且屬性值是以下格式函數的對象
    then: (resolve, reject) => { resolve(200) } } // thenable 對象能夠轉換爲 Promise 對象
const promise = Promise.resolve(thenable) promise.then(data => { // ...
})

上面的代碼就將一個thenalbe對象轉換爲一個Promise對象,只不過這裏沒有異步操做,全部的都會同步執行,可是不會報錯的。

其實,在咱們的平常開發中,這種將thenable轉換爲Promise的需求並很少。真正須要的是,將一些異步操做函數(如fs.readFile)轉換爲Promise(就像文章一開始readFilePromise作的那樣)。這塊,咱們後面會在介紹Q.js庫時,告訴你們一個簡單的方法。

其餘

以上都是一些平常開發中很是經常使用的功能,其餘詳細的介紹,請參考阮一峯老師的 ES6 教程 Promise 篇

最後,本節咱們只是介紹了Promise的一些應用,通俗易懂拿來就用的東西,可是沒有提高到理論和標準的高度。有人可能會不屑 ———— 我會用就好了,要那麼空談的理論幹嗎?———— 你只會使用卻上升不到理論高度,永遠都是個搬磚的,搬一塊磚掙一毛錢,不搬就不掙錢! 在我看來,全部的知識應該都須要上升到理論高度,將實際應用和標準對接,知道真正的出處,才能走的長遠。

下一節咱們介紹 Promise/A+ 規範

 

對標一下 Promise/A+ 規範

Promise/A 是由 CommonJS 組織制定的異步模式編程規範,後來又通過一些升級,就是當前的 Promise/A+ 規範。上一節講述的Promise的一些功能實現,就是根據這個規範來的。

本節內容概述

  • 介紹規範的核心內容
  • 狀態變化
  • then方法
  • 接下來...

介紹規範的核心內容

網上有不少介紹 Promise/A+ 規範的文章,你們能夠搜索來看,可是它的核心要點有如下幾個,我也是從看了以後本身總結的

關於狀態

  • promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
  • promise 的狀態只可能從「等待」轉到「完成」態或者「拒絕」態,不能逆向轉換,同時「完成」態和「拒絕」態不能相互轉換

關於then方法

  • promise 必須實現then方法,並且then必須返回一個 promise ,同一個 promise 的then能夠調用屢次(鏈式),而且回調的執行順序跟它們被定義時的順序一致
  • then方法接受兩個參數,第一個參數是成功時的回調,在 promise 由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在 promise 由「等待」態轉換到「拒絕」態時調用

下面挨個介紹這些規範在上一節代碼中的實現,所謂理論與實踐相結合。在閱讀如下內容時,你要時刻準備參考上一節的代碼。

狀態變化

promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)

拿到上一節的readFilePromise函數,而後執行const result = readFilePromise(someFileName)會獲得一個Promise對象。

  • 剛剛建立時,就是 等待(pending)狀態
  • 若是讀取文件成功了,readFilePromise函數內部的callback中會自定調用resolve(),這樣就變爲 已完成(fulfilled)狀態
  • 若是很不幸讀取文件失敗了(例如文件名寫錯了,找不到文件),readFilePromise函數內部的callback中會自定調用reject(),這樣就變爲 已拒絕(rejeced)狀態

promise 的狀態只可能從「等待」轉到「完成」態或者「拒絕」態,不能逆向轉換,同時「完成」態和「拒絕」態不能相互轉換

這個規則仍是能夠參考讀取文件的這個例子。從一開始準備讀取,到最後不管是讀取成功或是讀取失敗,都是不可逆的。另外,讀取成功和讀取失敗之間,也是不能互換的。這個邏輯沒有任何問題,很好理解。

then方法

promise 必須實現then方法,並且then必須返回一個 promise ,同一個 promise 的then能夠調用屢次(鏈式),而且回調的執行順序跟它們被定義時的順序一致

  • promise對象必須實現then方法這個無需解釋,沒有then那就不叫promise
  • 「並且then必須返回一個promise,同一個 promise 的then能夠調用屢次(鏈式)」 ———— 這兩句話說明了一個意思 ———— then確定要再返回一個promise,要否則then後面怎麼能再鏈式的跟一個then呢?

then方法接受兩個參數,第一個參數是成功時的回調,在 promise 由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在 promise 由「等待」態轉換到「拒絕」態時調用

這句話比較好理解了,咱們從一開始就在 demo 中演示。

接下來...

Promise的應用、規範都介紹完了,看起來挺牛的,也解決了異步操做中使用callback帶來的不少問題。可是Promise本質上究竟是一種什麼樣的存在,它是真的把callback棄而不用了嗎,仍是二者有什麼合做關係?它究竟是真的神通廣大,仍是使用了障眼法?

這些問題,你們學完Promise以後應該去思考,不能光學會怎麼用就中止了。下一節咱們一塊兒來探討~

Promise 真的取代 callback 了嗎

Promise 雖然改變了 JS 工程師對於異步操做的寫法,可是卻改變不了 JS 單線程、異步的執行模式。

本節概述

  • JS 異步的本質
  • Promise 只是表面的寫法上的改變
  • Promise 中不能缺乏 callback
  • 接下來...

JS 異步的本質

從最初的 ES三、4 到 ES5 再到如今的 ES6 和即將到來的 ES7,語法標準上更新不少,可是 JS 這種單線程、異步的本質是沒有改變的。nodejs 中讀取文件的代碼一直均可以這樣寫

fs.readFile('some.json', (err, data) => { })

既然異步這個本質不能改變,伴隨異步在一塊兒的永遠都會有callback,由於沒有callback就沒法實現異步。所以callback永遠存在。

Promise 只是表面的寫法上的改變

JS 工程師不會討厭 JS 異步的本質,可是很討厭 JS 異步操做中callback的書寫方式,特別是遇到萬惡的callback-hell(嵌套callback)時。

計算機的抽象思惟和人的具象思惟是徹底不同的,人永遠喜歡看起來更加符合邏輯、更加易於閱讀的程序,所以如今特別強調代碼可讀性。而Promise就是一種代碼可讀性的變化。你們感覺一下這兩種不一樣(這其中還包括異常處理,加上異常處理會更加複雜)

第一種,傳統的callback方式

fs.readFile('some1.json', (err, data) => { fs.readFile('some2.json', (err, data) => { fs.readFile('some3.json', (err, data) => { fs.readFile('some4.json', (err, data) => { }) }) }) })

第二種,Promise方式

readFilePromise('some1.json').then(data => { return readFilePromise('some2.json') }).then(data => { return readFilePromise('some3.json') }).then(data => { return readFilePromise('some4.json') })

這兩種方式對於代碼可讀性的對比,很是明顯。可是最後再次強調,Promise只是對於異步操做代碼可讀性的一種變化,它並無改變 JS 異步執行的本質,也沒有改變 JS 中存在callback的現象。

Promise 中不能缺乏 callback

上文已經基本給出了上一節提問的答案,可是這裏還須要再加一個補充:Promise不只僅是沒有取代callback或者棄而不用,反而Promise中要使用到callback。由於,JS 異步執行的本質,必須有callback存在,不然沒法實現。

再次粘貼處以前章節的封裝好的一個Promise函數(進行了一點點簡化)

const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { resolve(data.toString()) }) }) }

上面的代碼中,promise對象的狀態要從pending變化爲fulfilled,就須要去執行resolve()函數。那麼是從哪裏執行的 ———— 還得從callback中執行resolve函數 ———— 這就是Promise也須要callback的最直接體現。

接下來...

一塊技術「火」的程度和第三方開源軟件的數量、質量以及使用狀況有很大的正比關係。例如爲了簡化 DOM 操做,jquery 風靡全世界。Promise 用的比較多,第三方庫固然就必不可少,它們極大程度的簡化了 Promise 的代碼。

接下來咱們一塊兒看看Q.js這個庫的使用,學會了它,將極大程度提升你寫 Promise 的效率。

使用 Q.js 庫

若是實際項目中使用Promise,仍是強烈建議使用比較靠譜的第三方插件,會極大增長你的開發效率。除了將要介紹的Q.js,還有bluebird也推薦使用,去 github 自行搜索吧。

另外,使用第三方庫不只僅是提升效率,它還讓你在瀏覽器端(不支持Promise的環境中)使用promise

本節展現的代碼參考這裏

本節內容概述

  • 下載和安裝
  • 使用Q.nfcallQ.nfapply
  • 使用Q.defer
  • 使用Q.denodeify
  • 使用Q.allQ.any
  • 使用Q.delay
  • 其餘

下載和安裝

能夠直接去它的 github 地址 (近 1.3W 的 star 數量說明其用戶羣很大)查看文檔。

若是項目使用 CommonJS 規範直接 npm i q --save,若是是網頁外鏈可尋找可用的 cdn 地址,或者乾脆下載到本地。

如下我將要演示的代碼,都是使用 CommonJS 規範的,所以我要演示代碼以前加上引用,之後的代碼演示就不重複加了。

const Q = require('q')

使用Q.nfcallQ.nfapply

要使用這兩個函數,你得首先了解 JS 的callapply,若是不瞭解,先去看看。熟悉了這兩個函數以後,再回來看。

Q.nfcall就是使用call的語法來返回一個promise對象,例如

const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfcall(fs.readFile, fullFileName, 'utf-8')  // 使用 Q.nfcall 返回一個 promise
result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })

Q.nfapply就是使用apply的語法返回一個promise對象,例如

const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfapply(fs.readFile, [fullFileName, 'utf-8'])  // 使用 Q.nfapply 返回一個 promise
result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })

怎麼樣,體驗了一把,是否是比直接本身寫Promise簡單多了?

使用Q.defer

Q.defer算是一個比較偏底層一點的 API ,用於本身定義一個promise生成器,若是你須要在瀏覽器端編寫,並且瀏覽器不支持Promise,這個就有用處了。

function readFile(fileName) { const defer = Q.defer() fs.readFile(fileName, (err, data) => { if (err) { defer.reject(err) } else { defer.resolve(data.toString()) } }) return defer.promise } readFile('data1.json') .then(data => { console.log(data) }) .catch(err => { console.log(err.stack) })

使用Q.denodeify

咱們在很早以前的一節中本身封裝了一個fs.readFilepromise生成器,這裏再次回顧一下

const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) } else { resolve(data.toString()) } }) }) }

雖然看着不麻煩,可是仍是須要不少行代碼來實現,若是使用Q.denodeify,一行代碼就搞定了!

const readFilePromise = Q.denodeify(fs.readFile)

Q.denodeify就是一鍵將fs.readFile這種有回調函數做爲參數的異步操做封裝成一個promise生成器,很是方便!

使用Q.allQ.any

這兩個其實就是對應了以前講過的Promise.allPromise.race,並且應用起來如出一轍,很少贅述。

const r1 = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') const r2 = Q.nfcall(fs.readFile, 'data2.json', 'utf-8') Q.all([r1, r2]).then(arr => { console.log(arr) }).catch(err => { console.log(err) })

使用Q.delay

Q.delay,顧名思義,就是延遲的意思。例如,讀取一個文件成功以後,再過五秒鐘以後,再去作xxxx。這個若是是本身寫的話,也挺費勁的,可是Q.delay就直接給咱們分裝好了。

const result = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') result.delay(5000).then(data => { // 獲得結果
 console.log(data.toString()) }).catch(err => { // 捕獲錯誤
 console.log(err.stack) })

其餘

以上就是Q.js一些最經常使用的操做,其餘的一些很是用技巧,你們能夠去搜索或者去官網查看文檔。

至此,ES6 Promise的全部內容就已經講完了。可是異步操做的優化到這裏沒有結束,更加精彩的內容還在後面 ———— Generator

 

 

 

 傳送門:JavaScript 異步編程的前世此生(下)

 

 

文章轉載:https://blog.csdn.net/sinat_17775997/article/details/70307956(感謝、尊重做者、鞠躬)

相關文章
相關標籤/搜索