Callback Hell
with Promise
這份Slides全部節錄至 Nolan Lawson 寫在 PouchDB 官方的 Blog。
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
Promises 的確解決了 Callback Hell 的問題,但絕對不只是排版上的問題。就像在 Redemption from Callback Hell 這場很棒的talk中所解釋的,callback真正的問題是他讓我們不用寫
return
或是throw
等關鍵字。但是,反觀這樣程式的整個流程就是以副作用(side effects)
為基礎:一個 function 順便去呼叫另外一個 function。
To me, promises are all about
code structure
andflow
.--- Nolan Lawson, 18 May 2015
// Bad Practice
remotedb.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
var docs = result.rows;
docs.forEach(function(element) {
localdb.put(element.doc).then(function(response) {
alert("Pulled doc with id " + element.doc._id + " and added to local db.");
}).catch(function (err) {
if (err.name == 'conflict') {
localdb.get(element.doc._id).then(function (resp) {
localdb.remove(resp._id, resp._rev).then(function (resp) {
// et cetera...
// Better Style
remotedb.allDocs(...).then(function (resultOfAllDocs) {
return localdb.put(...);
}).then(function (resultOfPut) {
return localdb.get(...);
}).then(function (resultOfGet) {
return localdb.put(...);
}).catch(function (err) {
console.log(err);
});
composing promises
(組合式promises)forEach
?// Bad Practice
// I want to remove() all docs
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// I naively believe all docs have been removed() now!
});
forEach
? (Cont.)// Better Style
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// All docs have really been removed() now!
});
這邊發生了什麽事呢? 基本上 Promise.all() 拿 array 的 promises 當作 input,然後他會在所有的 promise 都 resolve 之後再將結果傳至 then()。他也等於是非同步的 for-loop。
.catch()
somePromise().then(function () {
return anotherPromise();
}).then(function () {
return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass
"deferred"
簡言之,Promise 有個很長的歷史與故事,他也花了 JS 社群很長的時間來將他規範到正確的方向。 在較早之前,jQuery 和 Angular 在每個地方都使用
deferred
模式,現在已經由ES6 Promise spec
所代替,其他的"好"
library 們也有實作這個功能,例如Q
,When
,RSVP
,Bluebird
,Lie
等等。所以如果你有想這麽做,那麽你就錯了。但該怎麽避免呢?
"deferred"
(Cont.)第一種:
大多 Promise libraries 都會讓你有辦法將
Promise
導入第三方的 libraries。
// ex. Angular's `$q`
$q.when(db.put(doc)).then(/* ... */); // <-- this is all the code you need
"deferred"
(Cont.)第二種:
利用 Revealing Constructor Pattern ,這種用法在包裝 非promise API 時很好用
new Promise(function (resolve, reject) {
fs.readFile('myfile.txt', function (err, file) {
if (err) {
return reject(err);
}
resolve(file);
});
}).then(/* ... */)
"deferred"
(Cont.)在 Bluebird
的文件中有解釋為什麽 使用"deferred"
是 anti-pattern。
In Deferred anti-pattern, "deferred" objects are created for no reason, complicating code.
// Bad Practice
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});
somePromise().then(function () {
// I'm inside a then() function!
});
在 then()
function 裡面我們應該做的有:
return
another promisereturn
a synchronous value (or undefined
)throw
a synchronous error第一種: Return another promise
最需要注意的是在第二個 promise 使用
return
,這個return
非常重要。 如果沒有使用return
的話,getUserAccountById()
就只是個副作用, 然後下一個 function 會收到undefined
而不是userAccount
。
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
第二種: Return a synchronous value (or undefined)
return
undefined
通常都是一個錯誤的作法,但 return 一個同步的值 是個很棒的方法來將同步的 code 轉成 promise-way 的 code。
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
第三種: Throw a synchronous error
講到
throw
,這就是 promise 很棒的其中一點。catch()
會在使用者登出時收到同步的錯誤,他也會收到任何 被reject
的同步錯誤。
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
Promise.resolve()
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);
可以透過 Promise.resolve()
簡短的寫成:
Promise.resolve(someSynchronousValue).then(/* ... */);
Promise.resolve()
因為這方法非常好用,可以將 promise-returning API methods 寫成:
function somePromiseAPI() {
return Promise.resolve().then(function () {
doSomethingThatMayThrow();
return 'foo';
}).then(/* ... */);
}
但千萬也別忘了 .catch()
他。
Promise.resolve()
同樣的,也可以使用 Promise.reject()
來馬上 reject 並 return promise
Promise.reject(new Error('some awful error'));
catch()
並不完全等於 then(null, ...)
在部落格中前段有提到 .catch()
是個 sugar,所以下列兩者會相等
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
catch()
並不完全等於 then(null, ...)
(Cont.)但並不代表下列兩者會相等
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
catch()
並不完全等於 then(null, ...)
(Cont.)如果不知道為什麽不一樣的話,throw
error 就知道了
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// I caught your error! :)
});
somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// I didn't catch your error! :(
});
catch()
並不完全等於 then(null, ...)
(Cont.)Another example of previous slide:
Promise.resolve(2).then((val) => {
if (val > 10) {
return val;
}
throw new Error('The number is too small');
})
.then((val) => console.log(val * 10))
.catch((err) => console.log('Error in catch method', err))
Promise.resolve(2).then((val) => {
if (val > 10) {
return val;
}
throw new Error('The number is too small');
}, (err) => {
console.log('Error in rejectHandler', err)
})
.then((val) => console.log(val * 10))
catch()
並不完全等於 then(null, ...)
(Cont.)如果寫 mocha 的 unit test 跑跑看的話
it('should throw an error', function () {
return doSomethingThatThrows().then(function () {
throw new Error('I expected an error!');
}, function (err) {
should.exist(err);
});
});
如果你想要執行一系列的行為在一個promise序列中,
換句話說,你可能會想要執行他們像 Promise.all()
,
但不是狀況下在平行的執行。
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
很不幸的,他們執行的不如預期。傳入 executeSequentially()
的
promises 還是平行的執行。
上一個 example 會這樣運作的原因是因為每個 promise 一被創造 就開始執行,所以你需要的是 an array of promise factories
function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
}
一個 promise factory 很簡單,因為他只是一個 function 回傳一個 promise
function myPromiseFactory() {
return somethingThatCreatesAPromise();
}
他為什麽能夠運作呢?因為他並不會創造一個 promise 直到他被執行時。
他就像個 then
function 一樣在運作,事實上,他們正是一樣的事情。
有時候,promise 會需要依靠其他 promise 所回傳的值,但是我們可能需要 兩個 promise 以上的值,例如:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// dangit, I need the "user" object too!
});
身為一個好的 JavaScript 工程師,應該會在上一層 scope 宣告一個 user
變數
var user;
getUserByName('nolan').then(function (result) {
user = result;
return getUserAccountById(user.id);
}).then(function (userAccount) {
// okay, I have both the "user" and the "userAccount"
});
這麽寫雖然沒錯,但是作者覺得這是個拙劣的設計。
所以作者覺得應該要這麽做 (這時應該要拋去成見,樂於接受金字塔)
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id).then(function (userAccount) {
// okay, I have both the "user" and the "userAccount"
});
});
如果排版對你來說真的是個 issue 的話,那麽你可以把他們都拉成 function
function onGetUserAndUserAccount(user, userAccount) {
return doSomething(user, userAccount);
}
function onGetUser(user) {
return getUserAccountById(user.id).then(function (userAccount) {
return onGetUserAndUserAccount(user, userAccount);
});
}
getUserByName('nolan')
.then(onGetUser)
.then(function () {
// at this point, doSomething() is done, and we are back to indentation 0
});
如果 promise 中的 code 開始變得複雜了,那麽也是應該把他們拉出來變成 function, 然後就變成漂亮的 code 了
putYourRightFootIn()
.then(putYourRightFootOut)
.then(putYourRightFootIn)
.then(shakeItAllAbout);
這段 code 會印出什麽?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
這段 code 會印出什麽?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
他會印出 foo
,如果你覺得是 bar
的話就錯了!
原因是因為當你傳一個 non-function (例如, 一個 promise) 給 then()
的時候,
他會直接幫你直譯成 then(null)
,因此就導致前一個範例的結果印失敗了,
你也可以試試看這個範例
Promise.resolve('foo').then(null).then(function (result) {
console.log(result);
});
在這邊又得提到前面 promises vs promise factories
所提到的。
簡言之,你可以
直接傳一個 promise 給 then()
,但他並不會執行的如你所想,
then()
應該接收一個 function 當作參數,所以大多情況你應該這麽做
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});
#
還記得第一頁的題目嗎?
再來讓我們公佈答案吧!
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
Answer:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);
Answer:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|
doSomething().then(doSomethingElse())
.then(finalHandler);
doSomething().then(doSomethingElse())
.then(finalHandler);
Answer:
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|
doSomething().then(doSomethingElse)
.then(finalHandler);
doSomething().then(doSomethingElse)
.then(finalHandler);
Answer:
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|