Javascript 在 ES6 新增了大量非常實用的功能,其中重要一項就是 Promsie,讓我們可以很直覺的處理非同步,在以前如果我們需要同時發出多個非同步請求,就必須在每次調用 function 時,一起在參數帶回 callback 的 function,重複了幾次就變成了波動拳。

接下來會用 promise 處理 callback hell,還有建立一個簡易的 promise,幫助我們理解 promise。

javascript callback hell
javascript callback hell

簡易的 Promise

複雜專案可能會出現的波動拳,這畫面我真實有看過…,假設換成用 promise 的話,就可以很輕鬆直覺處理掉,首先我們先建立一個簡單的 ajax function sample code,下面會用 es6 來寫,希望能在整個流程中,讓你了解 es6 的方便。

宣告一個 getData arrow function,裡面包含 XMLHttpRequest,我們監聽 onreadystatechange,當整個成功取得資料,就調用 resolve 來進行 callback 把資料放進 resolve function 傳遞,當取得資料失敗就調用 reject function 來傳遞資料。

當我們 new 一個 promise 的同時,我們 callback function 是帶入 function(resolve, reject){resolve or reject},讓內部 promise function 被我們用 resolve 或是 reject 調用。

  • promise 三種狀態
擱置(pending):初始狀態,不是 fulfilled 與 rejected。 實現(fulfilled):表示操作成功地完成。 拒絕(rejected):表示操作失敗了。

promise 會等待佇列 pending 狀態,等到被 resolve 觸發 fulfilled,就會開始回調 then,或是被 reject 觸發 catch。

then 以及 catch 都會回傳一個 promise,也就是說可以.then(()=>{}).then(()=>{}).then(()=>{})除非有錯誤產生,否則會往下調用下去。.catch(()=>{}).catch(()=>{}).catch(()=>{})則是當 javascript 有錯誤發生,會開始向下 catch,直到沒錯誤為止才會調用 catch function,同時因為沒錯誤所以會停住。

  • 非同步 function 範例
// declare arrow function return Promise // ** new Promise to inherit Promise instance ** const getData = (url) => new Promise((resolve, reject) => { // create http request const xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = () => { console.log(xhttp.readyState) if (xhttp.readyState === 4 ) { if (xhttp.status === 200) { // resolve will trigger Promise then callback console.log(JSON.parse(xhttp.response)); resolve(JSON.parse(xhttp.response)); } else { // reject will trigger Promise catch callback reject(xhttp.statusText); } } }; xhttp.open("GET", url, true); xhttp.send(); }); getData('https://jsonplaceholder.typicode.com/todos/1') .then(res => { // destructuring object const { id, title, completed } = res; const html = `<div class="item"> <p>Id: ${id}</p> <div>Title: ${title}</div> <div>Completed: ${completed}</div> </div>`; const newNode = document.createElement('div'); newNode.innerHTML = html; document.querySelector('#list').appendChild(newNode); } ) // it will run when promise Reject or // in then function appear javascript error .catch(res => { console.log(res); alert(`Something Error ,because ${res}`); });

codepen promise demo

Promise 解決 callback hell

那如果我們要繼續拉第二筆資料 /todos/2,這時候就能展現 promise 方便了,當拉完資料後,再回傳一個 promise,再用 then catch 去接受回傳值,反覆下去延伸。

這樣的寫法優點是比起以往依賴 callback 更直覺,另外每次都分同步取資料都可能會失敗。也很容易針對每個段點做不同的錯誤處理。

  • add more callback
... getData('https://jsonplaceholder.typicode.com/todos/1') .then(res => { appendItem(res); // return getData promise return getData('https://jsonplaceholder.typicode.com/todos/2') } ) .catch(res => { console.log('top', res); }) // it will start next promise then catch .then(res => { appendItem(res); return getData('https://jsonplaceholder.typicode.com/todos/3') } ).catch(res => { console.log('middle', res); }) .then(res => { appendItem(res); } ).catch(res => { console.log('bottom', res); }) // append html function const appendItem = (res) => { // prevent get null or undefined trigger .catch if (!res) { return; } const { id, title, completed } = res; const html = `<div class="item"> <div>Id: ${id}</div> <div>Title: ${title}</div> <div>Completed: ${completed}</div> </div>`; const newNode = document.createElement('div'); newNode.innerHTML = html; document.querySelector('#list').appendChild(newNode); }

codepen promise demo

實現 promise

promise 就像是個魔術,直到 es6 以前都很難處理好分同步處理,我們來試著做一個只單純帶有 then catch 簡易的 promise,來幫助我們更了解 promise。

先來解讀一下 promise,他是依賴 resolve、reject function 調用的,直接 new Promise(()=>{}),可以看到 promise 內部的狀態,有 status、value、then、catch、finally,這邊就先不理會 finally。

  • Promise native prototype
Promise {<pending>} __proto__: Promise catch: ƒ catch() constructor: ƒ Promise() finally: ƒ finally() then: ƒ then() Symbol(Symbol.toStringTag): "Promise" __proto__: Object [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

首先先用 es6 class 建立一個 promise,依照 promise 內部 code,我們也建立內部的變數 status 以及 value,status 是讓我們判斷 pending 或是 fullfill reject 狀態,value 則是用來接帶進來的值。

這邊比較容易看不懂的是 constructor(callback),這個 callback 指的是 new Promise( (res,rej)=>{ res('Hello')} ),簡單講就是你帶進來的 function。我們在使用 promise 會用到兩個 function,reslove 以及 reject,我們也依樣畫葫蘆,依樣命名一個 resolve、reject,依照(resolve,reject)順序帶入 callback,帶進來讓使用者可以調用到。當我們在外部使用第一個 function,就會調用到內部的 reslove,如果是第二個的話則是調用到內部的 reject。

距離實際的 promise 還缺少了 then、catch,接下來再繼續實作。

  • build promise
class promise { constructor(callback){ // promise status this.status = 'pending'; // create variable to store resolve or reject value this.value; // resolve is not outside resolve // it is use to pass callback function first function const resolve = res => { if(this.status === 'pending'){ this.status = 'fullfilled'; this.value = res; } } // reject is not outside reject // it is use to pass callback function second function const reject = res => { if(this.status !== 'pending'){ this.status = 'rejected'; this.value = res; } } // it's keypoint to call reslove or reject function // reslove or reject just assign status and value try { // this resolve is upper resolve function callback(resolve, reject); } catch (err) { reject(err); } } }
  • 加上 then catch 接受回傳值
... // then will let user call to check status then = (success, failed) => { console.log(`then`) if(this.status === 'fulfilled'){ return success(this.value); } if(this.status === 'rejected'){ return failed(this.value); } } // it will call then function and callback second callback function catch = cb => { this.then(null,cb); } }

codepen promise build

到這裡只是很簡單的 promise 大致上執行邏輯而已,方便我們更好理解 promise 原理,要完整實現還有很多細節要處理,例如說要 then 或是 catch 要 return promise,還有 all race finally 沒有寫上去。你可以看一下更多完整功能要怎實現出來。

vkarpov15/promise.js

雖然每次用 promise 都很理所當然,規則已經既定是如此,但如果每次使用都能了解背後原理,能夠以不同角度來看待,我自己覺得對我來說,幫助都很大,最近正在無盡的的優化網頁效能,無限感慨中…。

如果有錯誤歡迎留言,感謝閱讀。