《JavaScript with Promise》
关于 promise
介绍的太多了,今天就推荐一本我收藏已久的小册子,Javascript with promise - douban
Run to completion and the Event loop
This chapter explains how async tasks/callbacks work.
As we all know, javascript runs on a single thread, which means you can only do one thing at one time. For the async tasks or callbacks, they are not part of the JavaScript. In other words, they run on a separate thread.
[run to completion]
Once the async tasks are done, they need to communicate with JavaScript. But JavaScript is single-threaded, you cannot interrupt the current executing code.
[event loop]
One way is to put the callback into a queue. When JavaScript is available, it will look up the queue and execute each callback in order. Unfortunately, we do know how long the callback will have to wait after putting into the queue.
Promise
A promise is an object that serves as a placeholder for the value of an operation.
A promise has three states
-
pending: the init state
-
fulfilled: the operation has completed
-
rejected: the operation could not be completed
When a promise is no longer pending, it is said to be settled. In other words, that promise has reached its final state and it can never be changed.
Common Practices
The Async Ripple Effect
As a general rule, any function that uses a promise should also return a promise
function fetchList() {
listAPI().then((data) => console.log(data))
}
// trigger error: fetchList() return undefined
fetchList().then(() => {})
Conditional Logic
Avoid this style of conditional async execution
function showMenu() {
if (!user.authenticated) {
user.login().then(showMenu)
return
}
// other things...
}
The problem is that showMenu()
may run asynchronously or synchronously. The proper pattern is shown below,
function showMenu() {
const p = (!user.authenticated) ? user.login() : Promise.resolve()
return p.then(function () {
// other things...
})
}
Parallel execution
const userP = ['tom', 'alice', 'bob'].map((name) => ajax(name))
const p = Promise.all(userP)
However, p
will be rejected if one of the userP
fails.
If we want to get all the settled promises regardless of whether they succeeded or failed, we can use Promise.allSettled()
Promise.allSettled = (pList) => {
const allP = pList.map((p) => {
return p.then(
(value) => {
return { value, state: 'fulfilled' }
},
(reason) => {
return { reason, state: 'rejected' }
}
)
})
return Promise.all(allP)
}
Sequential execution
Using a loop
function sequence(arr, asyncFunc) {
return arr.reduce((p, val) => {
return p.then(() => asyncFunc(val))
}, Promise.resolve())
}
Using recursion
function sequence(arr, asyncFunc) {
const step = (idx) => {
if (idx == arr.length) return Promise.resolve()
return Promise.resolve(asyncFunc(val)).then(() => step(idx + 1))
}
step(0)
}
There are a little differences between the two methods.
-
Using a loop builds an entire promise chain when the
sequence()
finishes, however, the recursive method builds the chain step by step. -
the recursive method doesn’t need a predefined number of steps based on the elements in an array. This pattern is widely used in Nodejs.
function readFile(file) {
const reader = file.getReader()
const chunks = []
pump()
const pump = () => {
return reader.read().then((ret) => {
if (ret.done) {
return chunks
}
chunks.push(ret.value)
return pump()
})
}
}
Race
Promise.race([p1, p2])
A common case is the competition between timer and request.
function getData() {
const fetchInfo = ajax()
const getFromCache = new Promise((resolve) => {
const cached = ''
setTimeout(() => {
resolve(cached)
}, 1000)
})
return Promise.race([fetchInfo, getFromCache])
}
function getData() {
const fetchInfo = ajax()
const timeout = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
}, 1000)
})
return Promise.race([fetchInfo, timeout])
}
Error Handling
Rejecting a promise
-
explicit rejection
-
throw an error(explicitly/implicitly) in any of the callbacks the promise invokes (i.e., any callback passed to the Promise constructor, then, or catch.)
new Promise((resolve, reject) => {
reject(new Error('Explicit rejection'))
})
new Promise((resolve, reject) => {
console.log(a) // implicitly throw an error
})
new Promise((resolve, reject) => {
throw new Error('explicit throw error')// explicitly throw an error
})
Any error that occurs in a function that returns a promise should be used to reject the promise instead of being thrown back to the caller.
This approach allows the caller to deal with any problems that arise by attaching a catch handler to the returned promise instead of surrounding the call in a try/catch block
就是说,要在 promise 内抛出错误,这样可以在 catch
中捕获,避免在调用 promise
的地方,使用 try/catch
Avoid this
function badFunc(url) {
var image;
image.src = url; // Error: image is undefined
return new Promise(function (resolve, reject) {
image.onload = resolve;
image.onerror = reject;
})
}
function a() {
try {
badFunc()
} catch (e) {
console.log(e) // TypeError: Cannot set properties of undefined (setting 'src')
}
}
Recommend this
function badFunc(url) {
return new Promise(function (resolve, reject) {
var image;
image.src = url; // Error: image is undefined
image.onload = resolve;
image.onerror = reject;
})
}
function a() {
// TypeError: Cannot set properties of undefined (setting 'src')
badFunc().catch((e) => console.log(e))
}
Passing Errors
When we have a chain of promises, it’s a common practice to add a catch
at the end of the chain.
Because one promise is rejected, all subsequent promises in this chain area rejected in a domino effect until an onRejected handler is found
但是使用 catch
有一点需要注意
The catch function returns a new promise similar to then, but the promise that catch returns is only rejected if the callback throws an error.
换句话说,如果希望继续在 promise chain
中传递错误,你必须明确在 catch
中抛出错误才行,
function catchMe() {
queryUser()
.then(() => queryInfo())
.catch((err) => {
throw err
})
}
Combine generator
主要是结合 generator
可以实现 async
async function loadImage() {
const imageData = await fetchImage()
console.log(imageData)
}