提到 JavaScript 異步編程,不少小夥伴都很迷茫,本人花費大約一週的業餘時間來對 JS 異步作一個完整的總結,和各位同窗共勉共進步!html
part1 基礎部分node
part2 jQuery的解決方案jquery
part3 ES6-Promisegit
part4 Generatores6
part5 async-awaitgithub
v6.x
版本中使用 async-awaitpart6 總結ajax
提醒:若是你是初學 js 的同窗,還沒有有太多項目經驗和基礎知識,請就此打住,不要看這篇教程chrome
我思考問題、寫文章通常都不按討論出牌,別人寫過的東西我不會再照着抄一遍。所以,後面全部的內容,都是我看了許多資料以後,我的從新思考提煉總結出來的,這確定不能算是初級教程。npm
若是你是已有 js 開發經驗,並瞭解異步的基礎知識,到這裏來想深刻了解一下Promise
Generator
和async-await
,那就太好了,很是歡迎。編程
首先記住一句話 —— 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()
須要傳入兩個參數進去,url
和success
,其中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
readFile
readdir
setTimeout
setInterval
最後,請思考,事件綁定是否是也是異步操做?例如$btn.on('click', function() {...})
。這個問題頗有意思,我會再後面的章節通過分析以後給出答案,各位先本身想一下。
$.ajax
這個函數各位應該都比較熟悉了,要完整的講解 js 的異步操做,就必須先從$.ajax
這個方法提及。
想要學到全面的知識,你們就不要着急,跟隨個人節奏來,而且相信我。我安排的內容,確定都是有用的,對主題無用的東西,我不會拿來佔用你們的時間。
$.ajax
$.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 也不會再後面進行改進
$.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
對象,這個對象就帶有done
和fail
的方法,而且是等着請求返回以後再去調用這是一個標誌性的改造,無論這個概念是誰最早提出的,它在 jquery 中首先大量使用並讓全球開發者都知道原來 ajax 請求還能夠這樣寫。這爲之後的Promise
標準制定提供了很大意義的參考,你能夠覺得這就是後面Promise
的原型。
記住一句話————雖然 JS 是異步執行的語言,可是人的思惟是同步的————所以,開發者老是在尋求如何使用邏輯上看似同步的代碼來完成 JS 的異步請求。而 jquery 的這一次更新,讓開發者在必定程度上獲得了這樣的好處。
以前不管是什麼操做,我都須要一股腦寫到callback
中,如今不用了。如今成功了就寫到done
中,失敗了就寫到fail
中,若是成功了有多個步驟的操做,那我就寫不少個done
,而後鏈式鏈接起來就 OK 了。
Promise
的關係以上的這段代碼,咱們還能夠這樣寫。即不用done
和fail
函數,而是用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 v1.5 版本開始,$.ajax
可使用相似當前Promise
的then
函數以及鏈式操做。那麼它究竟是如何實現的呢?在此以前所用到的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
fail
和then
方法(不明白的去看上一節)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
fail
和then
方法(上一節說過),它還有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 的deferred
對象是解決了異步中callback
函數的問題,可是
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 dtd
。dtd
是一個deferred
對象,而dtd.promise
就是一個promise
對象。
promise
對象和deferred
對象最重要的區別,記住了————promise
對象相比於deferred
對象,缺乏了.resolve
和.reject
這倆函數屬性。這麼一來,可就徹底不同了。
上一節咱們提到一個問題,就是在程序的最後一行加一句w.reject()
會致使亂套,你如今再在最後一行加w.reject()
試試 ———— 保證亂套不了 ———— 而是你的程序不能執行,直接報錯。由於,w
是promise
對象,不具有.reject
屬性。
promise
的好處上一節提到deferred
對象有兩組屬性函數,並且提到應該把這兩組完全分開。如今經過上面一行代碼的改動,就分開了。
waitHandle
函數內部,使用dtd.resolve()
來該表狀態,作主動的修改操做waitHandle
最終返回promise
對象,只能去被動監聽變化(then
函數),而不能去主動修改操做一個「主動」一個「被動」,徹底分開了。
jquery v1.5 版本發佈時間距離如今(2018年)已經老早以前了,那會兒你們網頁標配都是 jquery 。不管裏面的deferred
和promise
這個概念和想法最先是哪位提出來的,可是最先展現給全世界開發者的是 jquery ,這算是Promise
這一律念最早的提出者。
其實本次課程主要是給你們分析 ES6 的Promise
Generator
和async-await
,可是爲什麼要從 jquery 開始(你們如今用 jquery 愈來愈少)?就是要給你們展現一下這段歷史的一些起點和發展的知識。有了這些基礎,你再去接受最新的概念會很是容易,由於全部的東西都是從最初順其天然發展進化而來的,咱們要去用一個發展進化的眼光學習知識,而不是死記硬背。
從 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
的一些比較常見的用法,敬請期待吧!
上一節對 ES6 的 Promise 有了一個最簡單的介紹,這一節詳細說一下 Promise 那些最多見的功能
Promise.all
和Promise.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.all
和Promise.race
的應用我還得繼續提出更加奇葩的需求,以演示Promise
的各個經常使用功能。以下需求:
讀取兩個文件data1.json
和data2.json
,如今我須要一塊兒讀取這兩個文件,等待它們所有都被讀取完,再作下一步的操做。此時須要用到Promise.all
// Promise.all 接收一個包含多個 promise 對象的數組 Promise.all([result1, result2]).then(datas => { // 接收到的 datas 是一個數組,依次包含了多個 promise 返回的內容 console.log(datas[0]) console.log(datas[1]) })
讀取兩個文件data1.json
和data2.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 是由 CommonJS 組織制定的異步模式編程規範,後來又通過一些升級,就是當前的 Promise/A+ 規範。上一節講述的Promise
的一些功能實現,就是根據這個規範來的。
then
方法網上有不少介紹 Promise/A+ 規範的文章,你們能夠搜索來看,可是它的核心要點有如下幾個,我也是從看了以後本身總結的
關於狀態
關於then
方法
then
方法,並且then
必須返回一個 promise ,同一個 promise 的then
能夠調用屢次(鏈式),而且回調的執行順序跟它們被定義時的順序一致then
方法接受兩個參數,第一個參數是成功時的回調,在 promise 由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在 promise 由「等待」態轉換到「拒絕」態時調用下面挨個介紹這些規範在上一節代碼中的實現,所謂理論與實踐相結合。在閱讀如下內容時,你要時刻準備參考上一節的代碼。
promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
拿到上一節的readFilePromise
函數,而後執行const result = readFilePromise(someFileName)
會獲得一個Promise
對象。
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 雖然改變了 JS 工程師對於異步操做的寫法,可是卻改變不了 JS 單線程、異步的執行模式。
從最初的 ES三、4 到 ES5 再到如今的 ES6 和即將到來的 ES7,語法標準上更新不少,可是 JS 這種單線程、異步的本質是沒有改變的。nodejs 中讀取文件的代碼一直均可以這樣寫
fs.readFile('some.json', (err, data) => { })
既然異步這個本質不能改變,伴隨異步在一塊兒的永遠都會有callback
,由於沒有callback
就沒法實現異步。所以callback
永遠存在。
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
。由於,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 的效率。
若是實際項目中使用Promise
,仍是強烈建議使用比較靠譜的第三方插件,會極大增長你的開發效率。除了將要介紹的Q.js
,還有bluebird
也推薦使用,去 github 自行搜索吧。
另外,使用第三方庫不只僅是提升效率,它還讓你在瀏覽器端(不支持Promise
的環境中)使用promise
。
本節展現的代碼參考這裏
Q.nfcall
和Q.nfapply
Q.defer
Q.denodeify
Q.all
和Q.any
Q.delay
能夠直接去它的 github 地址 (近 1.3W 的 star 數量說明其用戶羣很大)查看文檔。
若是項目使用 CommonJS 規範直接 npm i q --save
,若是是網頁外鏈可尋找可用的 cdn 地址,或者乾脆下載到本地。
如下我將要演示的代碼,都是使用 CommonJS 規範的,所以我要演示代碼以前加上引用,之後的代碼演示就不重複加了。
const Q = require('q')
Q.nfcall
和Q.nfapply
要使用這兩個函數,你得首先了解 JS 的call
和apply
,若是不瞭解,先去看看。熟悉了這兩個函數以後,再回來看。
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.readFile
的promise
生成器,這裏再次回顧一下
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.all
和Q.any
這兩個其實就是對應了以前講過的Promise.all
和Promise.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
文章轉載:https://blog.csdn.net/sinat_17775997/article/details/70307956(感謝、尊重做者、鞠躬)