關(guān)于Js的promise爬凑、generator徙缴、async、await
第一章 前言
? 大家都知道Javascript是單線程的嘁信,而且他的耗時(shí)操作是異步的于样,比如網(wǎng)絡(luò)請(qǐng)求以及IO操作。在一般來(lái)說(shuō)潘靖,我們比較喜歡他的異步穿剖,因?yàn)楫惒叫时容^高,資源得到了合理的利用卦溢,但是有的時(shí)候我們?yōu)榱丝刂屏鞒毯啵伊鞒汤锩娲嬖谝恍┖臅r(shí)操作秀又,如果還是使其異步的話就會(huì)使得我們的流程非常難控制,所以這個(gè)時(shí)候我們就要同步執(zhí)行我們的耗時(shí)操作啄刹。
第二章 關(guān)于單線程
? 有的時(shí)候我會(huì)想涮坐,javascript既然是單線程的,那為什么他又可以異步的呢誓军?(因?yàn)樽鳛樾“椎奈业恼J(rèn)知來(lái)說(shuō)袱讹,異步就是開(kāi)個(gè)新的線程去執(zhí)行這個(gè)耗時(shí)任務(wù) o(╥﹏╥)o)
? 這里就有一個(gè)主線程
的概念了,所謂單線程
就是Javascript 引擎在解釋昵时、處理javascript代碼的線程只有一個(gè)捷雕,這個(gè)線程就是主線程
。實(shí)際上瀏覽器還存在其他線程壹甥,比如處理網(wǎng)絡(luò)請(qǐng)求救巷、處理DOM等的線程,這些線程稱為工作線程
句柠,這里說(shuō)的單線程
的意思是javascript無(wú)論什么時(shí)候都只有一個(gè)線程在運(yùn)行javascript程序
這樣的好處就是javascript的單線程簡(jiǎn)化了處理事件的機(jī)制浦译,不必理會(huì)資源競(jìng)爭(zhēng)和線程同步這些復(fù)雜的問(wèn)題。
第三章 關(guān)于同步溯职、異步精盅、阻塞、非阻塞
同步和異步關(guān)注的是消息通信機(jī)制 (synchronous communication/ asynchronous communication)
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息谜酒,返回值)時(shí)的狀態(tài).
-
同步
同步就是程序一行一行的執(zhí)行代碼叹俏,只有等上一行的代碼執(zhí)行完了,才會(huì)繼續(xù)執(zhí)行下一行代碼
function sync{ console.log("1") console.log("2") console.log("3") console.log("4") console.log("5") } sync() // 輸出 1僻族,2粘驰,3,4述么,5
-
阻塞
阻塞調(diào)用是指在返回結(jié)果之前蝌数,程序會(huì)等待這個(gè)調(diào)用直到這個(gè)調(diào)用返回結(jié)果,才會(huì)繼續(xù)往下執(zhí)行
function block(){ console.log(1); console.log(2); // 這里假設(shè)這個(gè)文件比較大度秘,需要花費(fèi)1分鐘才能打開(kāi)它 let res = fs.readFileSync("xxx.json") console.log(3); console.log(4); } block() // 輸出 1籽前,2,(這里過(guò)了1分鐘之后) 繼續(xù)輸出 3敷钾,4
-
異步
異步操作在js中的原理是當(dāng)遇到異步操作時(shí)(比如網(wǎng)絡(luò)請(qǐng)求、IO耗時(shí)操作等),這個(gè)異步任務(wù)會(huì)掛起肄梨,放到
任務(wù)隊(duì)列
阻荒,任務(wù)隊(duì)列的任務(wù)會(huì)等到任務(wù)隊(duì)列之外的所有代碼執(zhí)行完畢之后在執(zhí)行,因此程序的執(zhí)行順序可能和代碼中的順序不一致众羡。function async() { console.log("開(kāi)始準(zhǔn)備請(qǐng)求數(shù)據(jù)"); console.log("馬上要開(kāi)始請(qǐng)求了..."); $.ajax('http://xxxx.com', function(resp) { console.log('請(qǐng)求完成~'); }); console.log("請(qǐng)求發(fā)出完成"); } async() // 開(kāi)始準(zhǔn)備請(qǐng)求數(shù)據(jù) // 馬上要開(kāi)始請(qǐng)求了... // 請(qǐng)求發(fā)出完成 // 請(qǐng)求完成~
-
非阻塞
非阻塞調(diào)用是指程序執(zhí)行到非阻塞調(diào)用時(shí)侨赡,會(huì)將該任務(wù)放置
任務(wù)隊(duì)列
,然后程序繼續(xù)往下執(zhí)行,等任務(wù)隊(duì)列
以外的代碼都執(zhí)行完成之后,才開(kāi)始執(zhí)行任務(wù)隊(duì)列
中的方法function nonBlocking(){ console.log("開(kāi)始讀文件了"); fs.readFile("./package.json",(err,data)=>{ console.log("文件讀取完成..."); }) console.log("發(fā)起文件讀取完畢"); } nonBlocking() // 開(kāi)始讀文件了 // 發(fā)起文件讀取完畢 // 文件讀取完成...
第四章 異步編程的四種方式
"異步模式"非常重要羊壹。在瀏覽器端蓖宦,耗時(shí)很長(zhǎng)的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng)油猫,最好的例子就是Ajax操作稠茂。在服務(wù)器端,"異步模式"甚至是唯一的模式情妖,因?yàn)閳?zhí)行環(huán)境是單線程的睬关,如果允許同步執(zhí)行所有http請(qǐng)求,服務(wù)器性能會(huì)急劇下降毡证,很快就會(huì)失去響應(yīng)电爹。 ------摘自 阮一峰
-
回調(diào)函數(shù)
/** * 吃飯 */ function eat(){ console.log("開(kāi)始吃飯"); } /** * 洗手 * @param {function} afterTask */ function wishHands(afterTask) { /** * 洗手要洗一分鐘 */ console.log("開(kāi)始洗手..."); setTimeout(_=>{ console.log("手洗干凈了..."); afterTask() },1000*60) } /** * 吃飯前需要洗手 */ wishHands(eat) // 開(kāi)始洗手... // 手洗干凈了... // 開(kāi)始吃飯
-
事件監(jiān)聽(tīng)
function task(){ console.log("task start"); setTimeout(_=>{ task.trigger('finish') // 觸發(fā)finish事件 },1000*3) } function afterTask(){ console.log("task finish"); } // 監(jiān)聽(tīng)finish事件 task.on('finish',afterTask) task()
這種方法的優(yōu)點(diǎn)是比較容易理解,可以綁定多個(gè)事件料睛,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù)丐箩,而且可以"去耦合"(Decoupling),有利于實(shí)現(xiàn)模塊化恤煞。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型屎勘,運(yùn)行流程會(huì)變得很不清晰。
-
發(fā)布/訂閱
我們假定阱州,存在一個(gè)"信號(hào)中心"挑秉,某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào)苔货,其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào)犀概,從而知道什么時(shí)候自己可以開(kāi)始執(zhí)行。這就叫做"發(fā)布/訂閱模式"(publish-subscribe pattern)夜惭,又稱"觀察者模式"(observer pattern)姻灶。
這個(gè)模式有多種實(shí)現(xiàn),下面采用的是Ben Alman的Tiny Pub/Sub诈茧,這是jQuery的一個(gè)插件产喉。
function task(){ console.log("task start"); setTimeout(_=>{ jQuery.publish("finish") },1000*3) } function afterTask(){ console.log("task finish"); } jQuery.subscribe('finish',afterTask) task()
-
Promise 對(duì)象
Promises對(duì)象是CommonJS工作組提出的一種規(guī)范,目的是為異步編程提供統(tǒng)一接口敢会。
它的思想是曾沈,每一個(gè)異步任務(wù)返回一個(gè)Promise對(duì)象,該對(duì)象有一個(gè)then方法鸥昏,允許指定回調(diào)函數(shù)塞俱。
function task(){ console.log("開(kāi)始執(zhí)行任務(wù)"); return new Promise((resolve,reject)=>{ setTimeout(_=>{ resolve("我完成啦") }) }) } function afterTask(){ console.log("afterTask 開(kāi)始"); } task().then(res=>{ console.log(res); afterTask() })
第五章 關(guān)于Promise
? Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大吏垮。它由社區(qū)最早提出和實(shí)現(xiàn)障涯,ES6 將其寫進(jìn)了語(yǔ)言標(biāo)準(zhǔn)罐旗,統(tǒng)一了用法,原生提供了Promise
對(duì)象唯蝶。
? 所謂Promise
九秀,簡(jiǎn)單說(shuō)就是一個(gè)容器,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果粘我。從語(yǔ)法上說(shuō)鼓蜒,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息涂滴。Promise 提供統(tǒng)一的 API友酱,各種異步操作都可以用同樣的方法進(jìn)行處理。
-
基本用法
Promise
對(duì)象是一個(gè)構(gòu)造函數(shù),接受一個(gè)包含resolve
回調(diào)和reject
回調(diào)參數(shù)的函數(shù)為參數(shù)柔纵,執(zhí)行結(jié)果符合預(yù)期可以調(diào)用resolve
缔杉,不符合預(yù)期可以執(zhí)行reject
拋出異常。let promise = new Promise((resolve,reject)=>{ let res = task() if (res is expect) { resolve("good") } reject("res is not expect") })
Promise有三種狀態(tài):
執(zhí)行中
搁料、已成功
或详、已失敗
resolve
調(diào)用之后,promise實(shí)例的狀態(tài)就從執(zhí)行中
->已成功
reject
調(diào)用之后郭计,promise實(shí)例的狀態(tài)就從執(zhí)行中
->已失敗
要對(duì)Promise的執(zhí)行結(jié)果做處理可以執(zhí)行它的
then
方法,then
方法包含兩個(gè)參數(shù)霸琴,第一個(gè)是成功的回調(diào),第二個(gè)是失敗可選回調(diào):promise.then(res=>{ console.log("exec success:"+res); // 這里res就是resolve的參數(shù) },err=>{ console.log("exec fail:"+err); // 這里 err就是reject的參數(shù) })
-
Promise 實(shí)例的屬性
promise.then(res=>{}) // 可以理解成結(jié)果的回調(diào) promise.catch(reason=>{}) // 執(zhí)行失敗或發(fā)生異常的回調(diào) promise.finally(_=>{}) // 執(zhí)行結(jié)束的回調(diào)昭伸,不管成功與否都會(huì)回調(diào) // then()方法和catch()返回的結(jié)果仍然是promise梧乘,所以可以使用鏈?zhǔn)綄懛? //寫法一 promise.then(res=>{ },err=>{ }).finally(_=>{ }) // 寫法二 promise.then(res=>{ }).catch(err=>{ }).finally(_=>{ }) // 寫法一和寫法二效果是一樣的
-
Promise靜態(tài)方法
-
Promise.all(...promise[])
將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例
Promise.all(p1,p2,p3)
上面代碼中庐杨,
Promise.all()
方法接受一個(gè)數(shù)組作為參數(shù)选调,p1
、p2
灵份、p3
都是 Promise 實(shí)例仁堪,如果不是,就會(huì)先調(diào)用下面講到的Promise.resolve
方法填渠,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例弦聂,再進(jìn)一步處理。另外氛什,Promise.all()
方法的參數(shù)可以不是數(shù)組莺葫,但必須具有 Iterator 接口,且返回的每個(gè)成員都是 Promise 實(shí)例枪眉。p
的狀態(tài)由p1
捺檬、p2
、p3
決定瑰谜,分成兩種情況欺冀。(1)只有
p1
、p2
萨脑、p3
的狀態(tài)都變成fulfilled
隐轩,p
的狀態(tài)才會(huì)變成fulfilled
,此時(shí)p1
渤早、p2
职车、p3
的返回值組成一個(gè)數(shù)組,傳遞給p
的回調(diào)函數(shù)鹊杖。(2)只要
p1
悴灵、p2
、p3
之中有一個(gè)被rejected
骂蓖,p
的狀態(tài)就變成rejected
积瞒,此時(shí)第一個(gè)被reject
的實(shí)例的返回值,會(huì)傳遞給p
的回調(diào)函數(shù)登下。-
Promise.race(...promise[])
方法同樣是將多個(gè) Promise 實(shí)例茫孔,包裝成一個(gè)新的 Promise 實(shí)例。
Promise.race(p1,p2,p3)
上面代碼中被芳,只要
p1
缰贝、p2
、p3
之中有一個(gè)實(shí)例率先改變狀態(tài)畔濒,p
的狀態(tài)就跟著改變剩晴。那個(gè)率先改變的 Promise 實(shí)例的返回值,就傳遞給p
的回調(diào)函數(shù)侵状。Promise.race()
方法的參數(shù)與Promise.all()
方法一樣赞弥,如果不是 Promise 實(shí)例,就會(huì)先調(diào)用下面講到的Promise.resolve()
方法壹将,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例嗤攻,再進(jìn)一步處理。- Promise.resolve(task) 將一個(gè)方法包裝成promise诽俯,比如他ajax請(qǐng)求
- Promise.reject(task) 同上
-
更多詳情請(qǐng)參考阮一峰
promise
第六章 關(guān)于Generator
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案妇菱,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同。
-
示例
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() // 什么都沒(méi)有 test.next() // generator test start // step 1 test.next() // step 2 test.next() // step 3 test.next() // generator test finish test.next() // 什么都沒(méi)有
? 從上面實(shí)例中我們可以看出來(lái)暴区,generator方法的聲明需要加上
*
號(hào)闯团,里面還有關(guān)鍵字yield
調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行仙粱,返回的也不是函數(shù)運(yùn)行結(jié)果房交,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象。然后需要執(zhí)行next
方法伐割,內(nèi)部指針就從函數(shù)頭部或上一次停下來(lái)的地方開(kāi)始執(zhí)行候味,直到遇到下一個(gè)yield
表達(dá)式(或return
語(yǔ)句)為止刃唤。換言之,Generator 函數(shù)是分段執(zhí)行的白群,yield
表達(dá)式是暫停執(zhí)行的標(biāo)記尚胞,而next
方法可以恢復(fù)執(zhí)行 更多詳情請(qǐng)參考阮一峰
generator
第七章 async和await
ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便帜慢。
async 函數(shù)是什么笼裳?一句話,它就是 Generator 函數(shù)的語(yǔ)法糖粱玲。
-
使用async與generator的對(duì)比
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()); }; // 一比較就會(huì)發(fā)現(xiàn)躬柬,async函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成async,將yield替換成await抽减,僅此而已允青。
-
基本用法
async
聲明的方法返回的是一個(gè)promise對(duì)象,可以使用then
方法添加回調(diào)函數(shù)胯甩,當(dāng)程序執(zhí)行的過(guò)程中昧廷,一旦遇到await
聲明的語(yǔ)句,程序?qū)?huì)等待這個(gè)語(yǔ)句返回結(jié)果之后才會(huì)執(zhí)行后面的方法偎箫。- 實(shí)例一:
// 這里假設(shè)getStudentIdByNumber和getStudentScoreById都是耗時(shí)的網(wǎng)絡(luò)請(qǐng)求木柬,所以是異步的 async function getStudentScoreByStudentNumber(studentNumber) { // 根據(jù)學(xué)號(hào)拿到id let studentId = await getStudentIdByNumber(studentNumber) // 通過(guò)id獲取分?jǐn)?shù) let score = await getStudentScoreById(studentId) // 獲取分?jǐn)?shù)后返回 return score } // 使用then去獲取執(zhí)行結(jié)果 getStudentScoreByStudentNumber("662297").then(score=>{ console.log(score); })
- 實(shí)例二:
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
命令后面是一個(gè) Promise 對(duì)象档插,返回該對(duì)象的結(jié)果漓雅。如果不是 Promise 對(duì)象州弟,就直接返回對(duì)應(yīng)的值暇咆。2、根據(jù)語(yǔ)法規(guī)則副硅,
await
命令只能出現(xiàn)在async
函數(shù)內(nèi)部姥宝,否則都會(huì)報(bào)錯(cuò)。3恐疲、
await
命令后面的Promise
對(duì)象腊满,運(yùn)行結(jié)果可能是rejected
,所以最好把await
命令放在try...catch
代碼塊中培己。
第八章 注意
如果多個(gè)異步請(qǐng)求之間存在前后關(guān)系碳蛋,可以像上一章一樣使用
await
來(lái)改造成同步-
如果多個(gè)
await
命令后面的異步操作,如果不存在繼發(fā)關(guān)系省咨,最好讓它們同時(shí)觸發(fā)肃弟。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
函數(shù)之中,如果用在普通函數(shù),就會(huì)報(bào)錯(cuò)笤受。-
如果將
forEach
方法中使用async
函數(shù)會(huì)有問(wèn)題// 錯(cuò)誤一 async function dbFuc(db) { let docs = [{}, {}, {}]; // 報(bào)錯(cuò) docs.forEach(function (doc) { await db.post(doc); }); } // 錯(cuò)誤二 function dbFuc(db) { //這里不需要 async let docs = [{}, {}, {}]; // 可能得到錯(cuò)誤結(jié)果 docs.forEach(async function (doc) { await db.post(doc); }); } // 改成for of 準(zhǔn)確 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); }