簡述非同步編程&Promise&非同步函數
- 2020 年 5 月 13 日
- 筆記
- Node.js學習筆記
前言:文章由本人在學習之餘總結鞏固思路,不足之前還請指出。
一.非同步編程
首先我們先簡單來回顧一下同步API和非同步API的概念
1.同步API:只有當前的API執行完成之前,才會執行下一個API
例:
console.log(『first'); console.log('last); 結果: first last
2.非同步API:當前API的執行不會阻塞後續程式碼的執行
例:
console.log('first'); setTimeout( () => { console.log('last'); }, 2000); console.log('middle'); 結果: first middle last
執行順序分析:
首先腳本會先執行同步程式碼,這時有一個同步程式碼區,按著從上到下的順序進行。當所有的同步程式碼執行完成之後,再進入非同步程式碼區查找是否有非同步程式碼,並開始執行,執行完之後,調用對應的回調函數。
拿例子中的計時器來說,該非同步函數每2秒都會重新調用一次。
注意:即使是計時器的時間設置為0,它任是一個非同步API,不會隨著同步一起執行。
3.Node.js中的非同步API
fs.readFile('./demo.txt', (err, result) => {});
當硬碟讀取了文件之後,調用後方的回調函數;
但這個時候,我們有了一個需求,依次讀取A,B,C三個文件。由於文件大小不一定是我們知道的,所以讀取,查找的速度我們也並不知道。
按照同步的思路來寫的話:
fs.readFile('./1.txt', 'utf8', (err, result1) => {console.log(result1)}); fs.readFile('./2.txt', 'utf8', (err, result2) => {console.log(result2)}); fs.readFile('./3.txt', 'utf8', (err, result3) => {console.log(result3)});
結果未必如我們所願。
這時我們也許會想到一個辦法,既然該API是非同步API,我們把各個API嵌套起來,這樣不久可以依次執行了?
fs.readFile('./1.txt', 'utf8', (err, result1) => { console.log(result1) fs.readFile('./2.txt', 'utf8', (err, result2) => { console.log(result2) fs.readFile('./3.txt', 'utf8', (err, result3) => { console.log(result3) }) }) });
emmm….確實可以,但他的缺點也能預料到:這裡只有三個文件,可能不易看出其劣勢,但是如果是300個呢?程式碼的可讀性將大幅度降低,維護的難度相應提高,這是我們不願意看到的。
這一現象,我們稱其為回調地獄(callbackhell)。進來了就si裡面了。
解決的辦法當然也有,這時該輪到我們的Promise登場了。
二.Promise
Promise出現的目的是解決Node.js非同步編程中回調地獄的問,它是一個構造函數,我們要用new Promise的方法調用。
我們先來簡單地介紹一下Promise。
let promise = new Promise((resolve, reject) => {});
Promise的參數為一個匿名回調函數,其中reslove,reject也是回調函數,他能將非同步API的執行和結果進行分離,reslove對應著result(正常思路下)當fs有返回結果的時候,我們可以將其通過回調函數的方式將其發送到外面,
同理,當fs出現錯誤的時候,我們可以將其發送到外面進行處理。這裡要用到Promise下面的兩個方法promise.then()&promise.catch(),分別用來對結果和錯誤資訊進行處理。
我們結合實例來分析這些回調函數。
let promise = new Promise((resolve, reject) => { fs.readFile('./1.txt', 'utf8', (err, result) => { if (err != null) { reject(err);
相當於執行then裡面的回調函數 }else { resolve(result);
相當於執行catch裡面的回調函數 } }); });
promise.then((result) => {
console.log(result);
})
.catch((err)=> {
console.log(err);
})
用此方法來對我們之前的函數進行包裝,分析一下,既然我們有三個非同步API,我們則需要用三個Promise將他們包裹起來,我們需要讓這三個promise依次執行,但是如果我們直接聲明了一個變數等於promise的話就直接執行了,所以
這裡我們用一個函數把他封裝起來
function p1 () { return new Promise ((resolve, reject) => { fs.readFile('./1.txt', 'utf8', (err, result) => { resolve(result) }) }); } function p2 () { return new Promise ((resolve, reject) => { fs.readFile('./2.txt', 'utf8', (err, result) => { resolve(result) }) }); } function p3 () { return new Promise ((resolve, reject) => { fs.readFile('./3.txt', 'utf8', (err, result) => { resolve(result) }) }); }
為了實現順序調用,我們使用鏈式編程
p1().then((r1)=> { console.log(r1); return p2(); }) .then((r2)=> { console.log(r2); return p3(); }) .then((r3) => { console.log(r3) })
注意:這裡return 調用返回的結果我們可以參考MDN的解釋
返回一個已經是接受狀態的 Promise,那麼 then
返回的 Promise 也會成為接受狀態,並且將那個 Promise 的接受狀態的回調函數的參數值作為該被返回的Promise的接受狀態回調函數的參數值。
這裡看上去我們的程式碼的似乎比之前的嵌套關係更為複雜,這裡就要引入我們的非同步函數了。
3.非同步函數
非同步函數是非同步編程語法的終極解決方案,它可以讓我們將非同步程式碼寫成同步的形式,讓程式碼不再有回調函數嵌套,使程式碼變得清晰明了。
舉一個簡單的函數來看看他的用法
// 1.在普通函數定義的前面加上async關鍵字 普通函數就變成了非同步函數 // 2.非同步函數默認的返回值是promise對象 // 3.在非同步函數內部使用throw關鍵字進行錯誤的拋出 async function fn () {
這裡我們發現我們不需要再new一個Promise再將其返回了 return 123; //正常的時候用return // throw '發生了一些錯誤';出錯的時候throw } console.log(fn ()) fn () .then(function (data) { console.log(data); }) .catch(function (err){ console.log(err); })
那麼如何讓我們的函數有序地進行呢?這裡我們不用再採用return嵌套,接下來就要用到await了
我們定義一個run函數
// await關鍵字 // 1.它只能出現在非同步函數中 // 2.await promise 它可以暫停非同步函數的執行 等待promise對象返回結果後再向下執行函數 async function run () { let r1 = await p1() let r2 = await p2() // await不能直接得到throw並賦值給r3這裡我們採用catch試試 let r3 = p3().catch(n => console.log(n));// p3 console.log(r1) console.log(r2) console.log(r3 instanceof Promise)//ture // console.log(r3) } 這樣就能實現我們的按順序執行了
此處r3的值是我在記筆記的時候發現await並不直接接受reject的Promise,所以做了個輸出的嘗試,隨意看看就好 run();
新手一枚,大家有啥好的想法或者問題歡迎一起討論