With the development of front-end, the word asynchronous is becoming more and more common. Suppose we now have such an asynchronous task:
Initiate several requests to the server, and the results of each request are used as parameters for the next request.
Let’s take a look at what we have to do:
Callbacks
The first thing that comes to mind and the most commonly used is the callback function. Let’s make a simple encapsulation:
let makeAjaxCall = (url, cb) => { // do some ajax // callback with result } makeAjaxCall('http://url1', (result) => { result = JSON.parse(result) })
Hmm, looks pretty good! But when we try to nest multiple tasks, the code looks like this:
makeAjaxCall('http://url1', (result) => { result = JSON.parse(result) makeAjaxCall(`http://url2?q=${result.query}`, (result) => { result = JSON.parse(result) makeAjaxCall(`http://url3?q=${result.query}`, (result) => { // ... }) }) })
Oh my God! Let that pile }) go to hell!
So, we want to try to use the JavaScript event model:
1. Pub/Sub
In the processing of DOM events, Pub/Sub is a very common mechanism. For example, we need to add event monitoring to elements:
elem.addEventListener(type, (evt) => { // handler })
So can we construct a similar model to handle asynchronous tasks?
The first thing is to build a distribution center and add the on / emit method:
let PubSub = { events: {}, on(type, handler) { let events = this.events events[type] = events[type] || [] events[type].push(handler) }, emit(type, ...datas) { let events = this.events if (!events[type]) { return } events[type].forEach((handler) => handler(...datas)) } }
Then we can use it like this:
const urls = [ 'http://url1', 'http://url2', 'http://url3' ] let makeAjaxCall = (url) => { // do some ajax PubSub.emit('ajaxEnd', result) } let subscribe = (urls) => { let index = 0 PubSub.on('ajaxEnd', (result) => { result = JSON.parse(result) if (urls[++index]) { makeAjaxCall(`${urls[index]}?q=${result.query}`) } }) makeAjaxCall(urls[0]) }
There seems to be no revolutionary change compared to the callback function, but the advantage of this is: we can put the request and processing functions in different modules to reduce coupling.
2. Promise
The real revolutionary change is the Promise specification. With Promise, we can complete asynchronous tasks like this:
let makeAjaxCall = (url) => { return new Promise((resolve, reject) => { // do some ajax resolve(result) }) } makeAjaxCall('http://url1') .then(JSON.parse) .then((result) => makeAjaxCall(`http://url2?q=${result.query}`)) .then(JSON.parse) .then((result) => makeAjaxCall(`http://url3?q=${result.query}`))
Great! It is written like a synchronous function!
Don’t worry, young man. We have even better:
3. Generators
Another big killer of ES6 is Generators[2]. In a generator function, we can interrupt the execution of the function through the yield statement, and iterate statements through the next method outside the function. More importantly, we can inject data into the function through the next method to dynamically change the behavior of the function. For example:
function* gen() { let a = yield 1 let b = yield a * 2 return b } let it = gen() it.next() // output: {value: 1, done: false} it.next(10) // a = 10, output: {value: 20, done: false} it.next(100) // b = 100, output: {value: 100, done: true}
Encapsulate our previous makeAjaxCall function through generator:
let makeAjaxCall = (url) => { // do some ajax iterator.next(result) } function* requests() { let result = yield makeAjaxCall('http://url1') result = JSON.parse(result) result = yield makeAjaxCall(`http://url2?q=${result.query}`) result = JSON.parse(result) result = yield makeAjaxCall(`http://url3?q=${result.query}`) } let iterator = requests() iterator.next() // get everything start
Oh! The logic seems very clear, but it feels so uncomfortable to have to inject iterator from the outside every time...
Don’t worry, let’s mix Promise and Generator and see what black magic will be produced:
let makeAjaxCall = (url) => { return new Promise((resolve, reject) => { // do some ajax resolve(result) }) } let runGen = (gen) => { let it = gen() let continuer = (value, err) => { let ret try { ret = err ? it.throw(err) : it.next(value) } catch (e) { return Promise.reject(e) } if (ret.done) { return ret.value } return Promise .resolve(ret.value) .then(continuer) .catch((e) => continuer(null, e)) } return continuer() } function* requests() { let result = yield makeAjaxCall('http://url1') result = JSON.parse(result) result = yield makeAjaxCall(`http://url2?q=${result.query}`) result = JSON.parse(result) result = yield makeAjaxCall(`http://url3?q=${result.query}`) } runGen(requests)
The runGen function looks like an automaton, so awesome!
Actually, this runGen method is an implementation of the ECMAScript 7 async function:
4. async function
In ES7, a more natural feature async function[3] is introduced. Using async function we can complete the task like this:
let makeAjaxCall = (url) => { return new Promise((resolve, reject) => { // do some ajax resolve(result) }) } ;(async () => { let result = await makeAjaxCall('http://url1') result = JSON.parse(result) result = await makeAjaxCall(`http://url2?q=${result.query}`) result = JSON.parse(result) result = await makeAjaxCall(`http://url3?q=${result.query}`) })()
Just like when we combined Promise and Generator above, the await keyword also accepts a Promise. In async function, the remaining statements will be executed only after the statement after await is completed. The whole process is just like we use the runGen function to encapsulate the Generator.
The above are several JavaScript asynchronous programming modes summarized in this article. I hope it will be helpful to everyone's learning.