關于Js的promise、generator、async、await
第一章 前言
? 大家都知道Javascript是單線程的,而且他的耗時操作是異步的,比如網絡請求以及IO操作。在一般來說,我們比較喜歡他的異步,因為異步效率比較高,資源得到了合理的利用,但是有的時候我們為了控制流程,而且流程里面存在一些耗時操作,如果還是使其異步的話就會使得我們的流程非常難控制,所以這個時候我們就要同步執行我們的耗時操作。
第二章 關于單線程
? 有的時候我會想,javascript既然是單線程的,那為什么他又可以異步的呢?(因為作為小白的我的認知來說,異步就是開個新的線程去執行這個耗時任務 o(╥﹏╥)o)
? 這里就有一個主線程
的概念了,所謂單線程
就是Javascript 引擎在解釋、處理javascript代碼的線程只有一個,這個線程就是主線程
。實際上瀏覽器還存在其他線程,比如處理網絡請求、處理DOM等的線程,這些線程稱為工作線程
,這里說的單線程
的意思是javascript無論什么時候都只有一個線程在運行javascript程序
這樣的好處就是javascript的單線程簡化了處理事件的機制,不必理會資源競爭和線程同步這些復雜的問題。
第三章 關于同步、異步、阻塞、非阻塞
同步和異步關注的是消息通信機制 (synchronous communication/ asynchronous communication)
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
-
同步
同步就是程序一行一行的執行代碼,只有等上一行的代碼執行完了,才會繼續執行下一行代碼
function sync{ console.log("1") console.log("2") console.log("3") console.log("4") console.log("5") } sync() // 輸出 1,2,3,4,5
-
阻塞
阻塞調用是指在返回結果之前,程序會等待這個調用直到這個調用返回結果,才會繼續往下執行
function block(){ console.log(1); console.log(2); // 這里假設這個文件比較大,需要花費1分鐘才能打開它 let res = fs.readFileSync("xxx.json") console.log(3); console.log(4); } block() // 輸出 1,2,(這里過了1分鐘之后) 繼續輸出 3,4
-
異步
異步操作在js中的原理是當遇到異步操作時(比如網絡請求、IO耗時操作等),這個異步任務會掛起,放到
任務隊列
,任務隊列的任務會等到任務隊列之外的所有代碼執行完畢之后在執行,因此程序的執行順序可能和代碼中的順序不一致。function async() { console.log("開始準備請求數據"); console.log("馬上要開始請求了..."); $.ajax('http://xxxx.com', function(resp) { console.log('請求完成~'); }); console.log("請求發出完成"); } async() // 開始準備請求數據 // 馬上要開始請求了... // 請求發出完成 // 請求完成~
-
非阻塞
非阻塞調用是指程序執行到非阻塞調用時,會將該任務放置
任務隊列
,然后程序繼續往下執行,等任務隊列
以外的代碼都執行完成之后,才開始執行任務隊列
中的方法function nonBlocking(){ console.log("開始讀文件了"); fs.readFile("./package.json",(err,data)=>{ console.log("文件讀取完成..."); }) console.log("發起文件讀取完畢"); } nonBlocking() // 開始讀文件了 // 發起文件讀取完畢 // 文件讀取完成...
第四章 異步編程的四種方式
"異步模式"非常重要。在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在服務器端,"異步模式"甚至是唯一的模式,因為執行環境是單線程的,如果允許同步執行所有http請求,服務器性能會急劇下降,很快就會失去響應。 ------摘自 阮一峰
-
回調函數
/** * 吃飯 */ function eat(){ console.log("開始吃飯"); } /** * 洗手 * @param {function} afterTask */ function wishHands(afterTask) { /** * 洗手要洗一分鐘 */ console.log("開始洗手..."); setTimeout(_=>{ console.log("手洗干凈了..."); afterTask() },1000*60) } /** * 吃飯前需要洗手 */ wishHands(eat) // 開始洗手... // 手洗干凈了... // 開始吃飯
-
事件監聽
function task(){ console.log("task start"); setTimeout(_=>{ task.trigger('finish') // 觸發finish事件 },1000*3) } function afterTask(){ console.log("task finish"); } // 監聽finish事件 task.on('finish',afterTask) task()
這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以"去耦合"(Decoupling),有利于實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
-
發布/訂閱
我們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發布"(publish)一個信號,其他任務可以向信號中心"訂閱"(subscribe)這個信號,從而知道什么時候自己可以開始執行。這就叫做"發布/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
這個模式有多種實現,下面采用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個插件。
function task(){ console.log("task start"); setTimeout(_=>{ jQuery.publish("finish") },1000*3) } function afterTask(){ console.log("task finish"); } jQuery.subscribe('finish',afterTask) task()
-
Promise 對象
Promises對象是CommonJS工作組提出的一種規范,目的是為異步編程提供統一接口。
它的思想是,每一個異步任務返回一個Promise對象,該對象有一個then方法,允許指定回調函數。
function task(){ console.log("開始執行任務"); return new Promise((resolve,reject)=>{ setTimeout(_=>{ resolve("我完成啦") }) }) } function afterTask(){ console.log("afterTask 開始"); } task().then(res=>{ console.log(res); afterTask() })
第五章 關于Promise
? Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise
對象。
? 所謂Promise
,簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。
-
基本用法
Promise
對象是一個構造函數,接受一個包含resolve
回調和reject
回調參數的函數為參數,執行結果符合預期可以調用resolve
,不符合預期可以執行reject
拋出異常。let promise = new Promise((resolve,reject)=>{ let res = task() if (res is expect) { resolve("good") } reject("res is not expect") })
Promise有三種狀態:
執行中
、已成功
、已失敗
resolve
調用之后,promise實例的狀態就從執行中
->已成功
reject
調用之后,promise實例的狀態就從執行中
->已失敗
要對Promise的執行結果做處理可以執行它的
then
方法,then
方法包含兩個參數,第一個是成功的回調,第二個是失敗可選回調:promise.then(res=>{ console.log("exec success:"+res); // 這里res就是resolve的參數 },err=>{ console.log("exec fail:"+err); // 這里 err就是reject的參數 })
-
Promise 實例的屬性
promise.then(res=>{}) // 可以理解成結果的回調 promise.catch(reason=>{}) // 執行失敗或發生異常的回調 promise.finally(_=>{}) // 執行結束的回調,不管成功與否都會回調 // then()方法和catch()返回的結果仍然是promise,所以可以使用鏈式寫法 //寫法一 promise.then(res=>{ },err=>{ }).finally(_=>{ }) // 寫法二 promise.then(res=>{ }).catch(err=>{ }).finally(_=>{ }) // 寫法一和寫法二效果是一樣的
-
Promise靜態方法
-
Promise.all(...promise[])
將多個 Promise 實例,包裝成一個新的 Promise 實例
Promise.all(p1,p2,p3)
上面代碼中,
Promise.all()
方法接受一個數組作為參數,p1
、p2
、p3
都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve
方法,將參數轉為 Promise 實例,再進一步處理。另外,Promise.all()
方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。p
的狀態由p1
、p2
、p3
決定,分成兩種情況。(1)只有
p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p
的回調函數。(2)只要
p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的實例的返回值,會傳遞給p
的回調函數。-
Promise.race(...promise[])
方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
Promise.race(p1,p2,p3)
上面代碼中,只要
p1
、p2
、p3
之中有一個實例率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給p
的回調函數。Promise.race()
方法的參數與Promise.all()
方法一樣,如果不是 Promise 實例,就會先調用下面講到的Promise.resolve()
方法,將參數轉為 Promise 實例,再進一步處理。- Promise.resolve(task) 將一個方法包裝成promise,比如他ajax請求
- Promise.reject(task) 同上
-
更多詳情請參考阮一峰
promise
第六章 關于Generator
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同。
-
示例
function* generatorTest(){ console.log("generator test start "); yield console.log("step 1"); yield console.log("step 2"); yield console.log("step 3"); console.log("generator test finish"); } let test = generatorTest() // 什么都沒有 test.next() // generator test start // step 1 test.next() // step 2 test.next() // step 3 test.next() // generator test finish test.next() // 什么都沒有
? 從上面實例中我們可以看出來,generator方法的聲明需要加上
*
號,里面還有關鍵字yield
調用 Generator 函數后,該函數并不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象。然后需要執行next
方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表達式(或return
語句)為止。換言之,Generator 函數是分段執行的,yield
表達式是暫停執行的標記,而next
方法可以恢復執行 更多詳情請參考阮一峰
generator
第七章 async和await
ES2017 標準引入了 async 函數,使得異步操作變得更加方便。
async 函數是什么?一句話,它就是 Generator 函數的語法糖。
-
使用async與generator的對比
const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; // 使用generator const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // 使用async const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // 一比較就會發現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。
-
基本用法
async
聲明的方法返回的是一個promise對象,可以使用then
方法添加回調函數,當程序執行的過程中,一旦遇到await
聲明的語句,程序將會等待這個語句返回結果之后才會執行后面的方法。- 實例一:
// 這里假設getStudentIdByNumber和getStudentScoreById都是耗時的網絡請求,所以是異步的 async function getStudentScoreByStudentNumber(studentNumber) { // 根據學號拿到id let studentId = await getStudentIdByNumber(studentNumber) // 通過id獲取分數 let score = await getStudentScoreById(studentId) // 獲取分數后返回 return score } // 使用then去獲取執行結果 getStudentScoreByStudentNumber("662297").then(score=>{ console.log(score); })
- 實例二:
function request(){ return new Promise((resolve,reject)=>{ setTimeout(resolve,1000*5) }) } async function networkTask(){ console.log("request start"); await request() console.log("request finish"); } networkTask() // 輸出 request start // 等待5秒 // 輸出 request finish
1、正常情況下,
await
命令后面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值。2、根據語法規則,
await
命令只能出現在async
函數內部,否則都會報錯。3、
await
命令后面的Promise
對象,運行結果可能是rejected
,所以最好把await
命令放在try...catch
代碼塊中。
第八章 注意
如果多個異步請求之間存在前后關系,可以像上一章一樣使用
await
來改造成同步-
如果多個
await
命令后面的異步操作,如果不存在繼發關系,最好讓它們同時觸發。let foo = await getFoo(); let bar = await getBar(); // 可寫成 // 寫法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 寫法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;
await
命令只能用在async
函數之中,如果用在普通函數,就會報錯。-
如果將
forEach
方法中使用async
函數會有問題// 錯誤一 async function dbFuc(db) { let docs = [{}, {}, {}]; // 報錯 docs.forEach(function (doc) { await db.post(doc); }); } // 錯誤二 function dbFuc(db) { //這里不需要 async let docs = [{}, {}, {}]; // 可能得到錯誤結果 docs.forEach(async function (doc) { await db.post(doc); }); } // 改成for of 準確 async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } } // 改成reduce async function dbFuc(db) { let docs = [{}, {}, {}]; await docs.reduce(async (_, doc) => { await _; await db.post(doc); }, undefined); }