Day.28 「Promise 初體驗~」 —— ES6 Promise

我們前面已經學習了回調函式(Callback Function)與構造函式(Constrcutor),而 Promise 是 ES6 新增用來解決非同步回調地域的新語法,同時也是一個構造函式!
非同步
在這裡我們要先了解到什麼是非同步!相信大家應該都聽過最好理解的範例,那就是用餐廳來做範例!
同步的概念就像是:服務生接收點餐 → 通知廚房有餐 → 廚房完成餐點 → 結帳 → 接下一位客人 一步一步做下去,優點是不易出錯,但缺點也非常明顯,效率非常差。
而非同步的概念:服務生接收點餐 → 通知廚房有餐 → 結帳 → 接下一位客人 → … → 廚房完成餐點一起給客人 能夠把需要先執行的優先執行,優點就是效率好,但缺點就是跟同步比起來,維護比較麻煩。
而在我們介紹定時器時,就有體現出非同步的狀態。
function order() {
console.log('點餐');
(function making() {
console.log('開始製作');
(function checkout() {
console.log('結帳')
})()
setTimeout(() => {
console.log('餐點完成')
}, 1000)
})()
}
order()
order()
/*
"點餐"
"開始製作"
"結帳"
"點餐"
"開始製作"
"結帳"
"餐點完成"
"餐點完成"
*/這時就有點看到回調地獄(Callback Hell)的影子了!這就是它不容易維護的部分
- 不易閱讀
- 處理異常處理不方便
而 Promise 改善了回調地獄的問題。
ES6 以前
在還沒有 ES6 前,處理 AJAX 與 計時器的時候,都是直接使用回調函式來處理非同步事件 這裡用抽獎為例
<!-- HTML -->
<button id="btn">點我抽獎</button>const btn = document.getElementById('btn') // 獲取按鈕 DOM
/* 隨機數函式 */
function randomNum(m, n) {
return Math.ceil(Math.random() * (n - m + 1)) + m - 1
}
btn.addEventListener('click', () => {
// 設定按按鈕後一秒後抽獎
setTimeout(() => {
const n = randomNum(1, 100) // 1~100 隨機數
if (n <= 30) {
console.log(`恭喜你中獎了!你的中獎數字是${n}`) // 30% 中獎率
}
else {
console.error(`銘謝惠顧~你的數字是${n}`)
}
}, 1000)
})Promise
在 ES6 之後,可以透過 Promise 來包裝程式碼,
而使用 Promise 的方式,與構造函式的使用方式類同,而參數帶入的是函式,帶入的函式內會有兩個參數 resolve 、 reject。
const p = new Promise((resolve, reject) => {
// ...
})這兩個參數本身也是函式,一個代表解決,一個代表拒絕,函式的參數可以進行傳遞。
const p = new Promise((resolve, reject) => {
if ('成功') {
resolve('成功') // 成功使用 resolve 函式,代表這個 Promise 物件的狀態是成功的
}
else {
reject('失敗') // 失敗使用 reject 函式,代表這個 Promise 物件的狀態是失敗的
}
})以上面的抽獎例子做修改。
/* 修改事件監聽,進行 Promise 包裝 */
btn.addEventListener('click', () => {
const p = new Promise((resolve, reject) => { // Promise 包裝
setTimeout(() => {
const n = randomNum(1, 100) // 1~100 隨機數
if (n <= 30) {
resolve(n) // 將 Promise 物件設定為"成功" n 作為資料參數傳遞出去
}
else {
reject(n) // 將 Promise 物件設定為"失敗" n 作為資料參數傳遞出去
}
}, 1000)
})
})這樣就包裝好了,但你會發現,奇怪怎麼沒有效果了?
那是因為還要調用 then 方法,來接收成功或失敗的資料,一樣可以接收兩個參數,兩個參數分別代表成功與失敗的函式,而成功與失敗的函式可以靠參數傳遞資料。
then
p.then((data) => {
console.log(`恭喜你中獎了!你的中獎數字是${data}`)
}, (err) => {
console.error(`銘謝惠顧~你的數字是${err}`)
})你可能覺得,好像沒有方便到哪裡呀~還要另外用 then 來調用!
那是因為我們這個範例還很簡單,沒有到 Callback Hell 的程度,當資料越來越複雜,就會形成 Callback Hell。
// node.js 資料串接
data.readFile('./data/a.text', (err, data1) => {
data.readFile('./data/b.text', (err, data2) => {
data.readFile('./data/c.text', (err, data3) => {
data.readFile('./data/d.text', (err, data4) => {
const result = data1 + data2 + data3 + data4
console.log(result)
})
})
})
})catch
而 Promise 只要包裝好了,接下來只要使用 then 來進行連續調用串接,不會讓程式碼越來越往右推移。
此外大多數情況,也不會刻意接失敗的資料,可以依靠 catch 來進行最後失敗時的處理
// 先在最外層進行 Promise 包裝
const p = new Promise((res, rej) => {
data.readFile('./data/a.text', (err, data) => {
res(data)
})
})
// 使用 then 串接
p.then((val) => {
return new Promise((res, rej) => {
data.readFile('./data/b.text', (err, data) => {
res([val, data])
})
})
}).then((val) => {
return new Promise((res, rej) => {
data.readFile('./data/c.text', (err, data) => {
val.push(data)
res(val)
})
})
}).then((val) => {
console.log(val) // 成功使用 then
}).catch((err) => {
console.error('串接失敗!') // 失敗使用 catch
})總結
雖然短期這樣看,Promise 寫起來好像沒有 Callback 快,但它解決了長期的資料變龐大的時候,所產生的回調地獄,Promise 只是向下添加程式碼,而 Callback Hell 則是一直往右推移程式碼,Promise 還有很多方法還沒講到,目前只是初體驗!
參考資料
Related Posts
Day.30 「什麼!? Promise 的語法糖?」 —— ES8 Async & Await
ES8 新增了 async 與 await 兩個語法,這兩個語法也是為了更方便解決非同步的語法,可以說是 Promise then 的語法糖。
Day.29 「Class 語法糖~」 —— ES6 Class 構造函式
ES6 新增了更接近傳統語言寫法的 Class 這個概念,基本上可以當作是一個**語法糖**,絕大部分的功能在 ES5 都可以做到,但 Class 可以讓構造函式的寫法更加清新!
Day.27 「var 成為時代的眼淚!」 —— ES6 區塊作用域 與 提升
之前已經介紹了 var 與函式作用域,今天來更仔細的介紹 ES6 新增的 let 、 const以及與它們相關的區塊作用域。
Johnson Mao
A passionate software engineer and blogger sharing thoughts on technology, AI, and development.