preface

Before reading this article, I want you to know:

  • JavaScriptEvent cycle of
  • Browser processes and threads
  • PromiseThe relevant knowledge

A brief description of asynchronous programming in JS

JS single threaded, synchronous and asynchronous

JS single-threaded

The execution environment of the Javascript language is “single-threaded”. This means you can only do one task at a time. If you have more than one task, you must queue the first one to complete and then the next one to execute.

Although this mode is relatively simple to implement and the execution environment is relatively simple, as long as one task takes a long time, the following tasks must wait in line, which will delay the execution of the whole program. A common browser non-response (suspended animation) is usually the result of a single piece of Javascript code running for so long (such as an infinite loop) that the entire page gets stuck in one place and no other task can be performed.

Why is JavaScript single threaded?

The single thread of JavaScript, relative to its purpose. As a browser scripting language, JavaScript’s primary purpose is to interact with users and manipulate the DOM. This means that it has to be single-threaded, which can cause complex synchronization problems. For example, if there are two threads of JavaScript at the same time, one thread adds content to a DOM node, and the other thread removes that node, which thread should the browser use?

So, to avoid complexity, JavaScript has been single-threaded since its inception.

WebWorker, JS multithreading?

MDN’s official explanation is:

Web workers provide an easy way for Web content to run scripts in background threads. Threads can perform tasks without interfering with the user interface. A worker is an object created using a constructor (e.g. worker ()) that runs a named JavaScript file. This file contains the code to be run in the worker thread; Workers runs in a different global context than the current onewindowTherefore, usewindowShortcuts that get the current global scope (instead of self) within a Worker will return an errorCopy the code

Think about it this way:

  • createWorkerThe javascript engine requests the browser to open a child thread (the child thread is opened by the browser, completely controlled by the main thread, and cannot manipulate the DOM).
  • The JS engine thread communicates with the worker thread in a specific way (postMessage API, which requires the thread to interact with specific data by serializing objects)

Therefore, if there is a very time-consuming work, please open a separate Worker thread, so that no matter how earthshaking inside will not affect the MAIN thread of JS engine, just wait to calculate the result, the result of communication to the main thread can, Perfect!

Also note that the JS engine is single-threaded, the essence of which remains unchanged. The Worker can understand that the browser is opening a plug-in to the JS engine, which is specially used to solve those massive computing problems.

Synchronous and asynchronous

  1. Synchronization: the continuous execution of multiple tasks without interruption, with blocking effect;
  2. Asynchronous: Perform multiple tasks discontinuously without blocking.

To put it simply: synchronous executes in your code order, asynchronous does not execute in code order.

1. Callback function

Callbacks are the most basic method for asynchronous operations. The following code is an example of a callback function:

ajax(url, () = > {
    // Process logic
})
Copy the code

But Callback functions have a fatal flaw: they are easy to write Callback hell. Assuming multiple requests have dependencies, you might write code like this:

ajax(url, () = > {
    // Process logic
    ajax(url1, () = > {
        // Process logic
        ajax(url2, () = > {
            // Process logic})})})Copy the code
  • Advantages: Simple, easy to understand and implement;

  • Disadvantages:

    • It is not conducive to code reading and maintenance, and the high coupling between various parts makes the program structure chaotic and flow difficult to track (especially in the case of multiple nested callback functions);
    • Only one callback function can be specified per task;
    • Do not use a try catch to catch an error. Do not return directly.
    • It is extremely easy to write Callback hell.

2. Event monitoring

The execution of an asynchronous task depends not on the order of the code, but on whether an event occurs.

For example, fnB can be executed only after fnA is executed. (jQuery is used here)

fnA.on('done', fnB) // Bind an event to the fnA. When a done event occurs in the fnA, fnB is executed.

function fnA() {
  setTimeout(function () {...// Process logic

    fnA.trigger('done') // The done event is triggered immediately after the fnB is executed.
  }, 1000)}Copy the code
  • Advantages:

    • Multiple events can be bound, and each event can specify multiple callback functions;
    • “De-coupling” is good for modularity.
  • Disadvantages:

    • The entire application becomes event-driven, and the flow becomes unclear
    • Poor readability, it is difficult to sort out the main process of the program.

3. Publish subscriptions

There is a “signal center” that “publishes” a signal when a task completes, and other tasks can “subscribe” to the signal center to know when they can start executing. This is called a publish-subscribe pattern, also known as an observer pattern.

For example, fnB triggers execution by subscribing to the DONE signal.

Watcher.subscribe('done', fnB) // fnB subscribes to the done signal from Watcher.

function fnA() {
  setTimeout(function () {...// Process logic

    Watcher.publish('done') // The done signal is sent to Watcher to trigger fnB execution.
  }, 1000)}function fnB(){...// Process logic

    Watcher.unsubscribe('done', fnB); // Unsubscribe after fnB completes execution
}
Copy the code
  • Advantages:

    • Support simple broadcast communication, when the object state changes, will automatically notify the subscribed objects;
    • The coupling between the publisher and the subscriber is reduced. The publisher only releases a message, and it does not care how the message is used by the subscriber. At the same time, the subscriber only listens to the event name of the publisher, and it does not care how the publisher changes as long as the event name of the publisher remains unchanged.
    • You can monitor the execution of your program by looking at the Message Center to see how many signals exist and how many subscribers each signal has.
  • Disadvantages:

    • Creating a “message center” takes time and memory;
    • Although it can weaken the relationship between objects, if overused, it will make the code less readable and maintainable.

4. Promise/A+

I could write a whole article about this, but I’m going to brief it here, because the main character is async/await

  1. I’ll make a Promise to you after a certain amount of time.

  2. Three states of Promise

    • Pending—-PromiseThe initial state of the object instance when it is created
    • Depressing —- can be understood as the state of success
    • Rejected—-

    Once the promise is changed from a wait state to another state, the state can never be changed

  3. The chain call to promise

    • Each call returns a new onePromiseInstance (this is why chain calls are available for THEN)
    • ifthenReturns a result that will be passed next timethenThe successful callback in
    • ifthenIf an exception occurs in, the next one will gothenThe failed callback
    • inthenThe use of thereturn, thenreturnValue isPromise.resolve()packaging
    • thenCan not pass the argument, if not passed through to the nextthenIn the
    • catchExceptions that are not caught are caught
  4. Rewrite the previous callback hell example as follows:

    ajax(url)
      .then(res= > {
          console.log(res)
          return ajax(url2) // Wrap promise.resolve (ajax(url2))
      }).then(res= > {
          console.log(res)
          return ajax(url3)
      }).then(res= > console.log(res))
    Copy the code
    • Advantages:

      • Fixed callback hell
      • The ability to catch errors through callback functions
    • Disadvantages:

      • Can’t cancelPromiseOnce created, it is executed and cannot be cancelled
      • If the callback function is not set, errors thrown inside a Promise are not reflected outside
      • When you are in a Pending state, you have no way of knowing what stage of progress you are currently in (just started or about to complete).

5. Generator

  1. The most important feature of the Generator is that it can control the execution of functions.

  2. ES6 introduces Generator functions that suspend the execution flow of functions using the yield keyword and switch to the next state using the next() method, providing the possibility to change the execution flow and thus providing a solution for asynchronous programming.

    • There is an asterisk between the function keyword and the function name.
    • Syntactically, the Generator function is a state machine that encapsulates multiple internal states.
    • In addition to the state machine, the Generator function is also an iterator object Generator.
    • The paused function, yield paused, and the next method started, returns the yield expression each time.
    • The yield expression itself returns no value, or always returns undefined. The next method can take an argument that is treated as the return value of the previous yield expression.
  3. Let’s start with an example:

    function *foo(x) {
      let y = 2 * (yield (x + 1))
      let z = yield (y / 3)
      return (x + y + z)
    }
    let it = foo(5)
    console.log(it.next())   // => {value: 6, done: false}
    console.log(it.next(12)) // => {value: 8, done: false}
    console.log(it.next(13)) // => {value: 42, done: true}
    Copy the code

    We analyzed it line by line:

    • First, a Generator call is different from a normal function in that it returns an iterator
    • When next is executed for the first time, the pass is ignored and the function pauses at yield (x + 1), so 5 + 1 = 6 is returned
    • When next is executed the second time, the passed argument 12 is treated as the return value of the previous yield expression. If you do not pass the argument, yield always returns undefined. Let y = 2 * 12, so the second yield is equal to 2 * 12/3 = 8
    • When next is executed the third time, the passed argument 13 is treated as the return value of the previous yield expression, so z = 13, x = 5, and y = 24 add up to 42
  4. It also solves the problem of callback hell.

    function *fetch() {
        yield ajax(urlA, () = > {})
        yield ajax(urlB, () = > {})
        yield ajax(urlC, () = >{})}let it = fetch()
    let result1 = it.next()
    let result2 = it.next()
    let result3 = it.next()
    Copy the code
    • Advantages:

      • Can be executed step by step and get the result of asynchronous operation;
      • Can know the process of asynchronous operation;
      • You can enter the process of modifying asynchronous operations.
    • Disadvantages:

      • Still need to use asynchronous thinking to read code;
      • Iterating over Generator functions manually is cumbersome.

6. Async/Await

This is the protagonist, and I’m going to talk about it in more detail

async function fetch() {
    await ajax(url1)
    await ajax(url2)
    await ajax(url3)
}
Copy the code

async/await

Async is short for ‘asynchronous’ and await can be thought of as short for async wait.

Async is used to declare that a function is asynchronous and await is used to wait for an asynchronous method to complete.

What is Async?

Async functions are syntactic sugar for Generator functions. Async is represented with the keyword async and await is used inside the function.

Async is a new feature in ES7 that indicates that the current function is asynchronous and does not block the thread causing subsequent code to stop running.

How does it work?

Once declared, the call can be made

async function asyncFn() {return 'hello world'
}
asyncFn()
Copy the code

Async returns a promise object. If function returns a value, async returns promise.resolve (). Resolve (undefined) if no value is returned

Promise.resolve(x) can be thought of as a shorthand for New Promise(resolve => resolve(x)) and can be used to quickly encapsulate literal objects or other objects as Promise instances.

If the function throws an exception internally or returns reject, the promise state of the function is failed REJECT.

async function e() {    
    throw new Error('1')
}
e().then(success= > console.log('success', success))   
   .catch(error= > console.log('failure', error)) / / 1

Copy the code

What does async do?

A function with the async keyword that makes your function return a promise object

That is:

  • If the async keyword function returns anything other than a promise, it is wrapped automatically in promise.resolve ().

  • If the async keyword function explicitly returns a promise, the promise you return takes effect.

    So if a function returns a promise itself, there is no need to use an async declaration.

What is await

Can only be used in functions defined using async.

‘await’ means to wait, so what is he waiting for? It says on the MDN:

[return_value] = await expression;
Copy the code

It’s an expression, so an expression can be a constant, a variable, a promise, a function.

Normally, the await command is followed by a Promise, and if it is not, it will be converted to an immediately resolve Promise

async function  f() {
    return await 1
}
f().then( v= > console.log(v)) / / 1
Copy the code

When an async function is await, the function behind await is await once (e.g. Fn of await Fn(), not the next line of code), and then the whole async function is exited to its thread to execute the js code. The thread resumes only after the promise-based asynchronous operation it is waiting for has been honored or rejected.

The state of the Promise returned by async functions will not change until all the Promise objects of the internal await command have been executed.

Why async/await

No need to solve callback hell.

Relative to thePromise

  1. Better handling of then chains

    • See asynchronous programming abovePromise,asyncCode:PromiseThis way is full ofthen()Method, if the process is complex, the whole code will be filledthen. The semantics are not obvious and the code flow does not represent the execution flow well.
    • How to stopPromiseThe chain, that’s the hard part, is the wholePromiseThe most complicated part. For example: Do you want to be in the firstthenI jump out of the chain, and I don’t want to do it.
  2. Pomise transfer parameters too much trouble, look very dizzy.

    For example, suppose a business is done in multiple steps, each of which is asynchronous and each of which requires the results of each of the previous steps.

    function takeLongTime(n) {
        return new Promise(resolve= > {
            setTimeout(() = > resolve(n + 200), n)
        })
    }
    
    function step1(n) {
        console.log(`step1 with ${n}`)
        return takeLongTime(n)
    }
    
    function step2(m, n) {
        console.log(`step2 with ${m} and ${n}`)
        return takeLongTime(m + n)
    }
    
    function step3(k, m, n) {
        console.log(`step3 with ${k}.${m} and ${n}`)
        return takeLongTime(k + m + n)
    }
    
    
    function doIt() {
        console.time("doIt")
        const time1 = 300;
        step1(time1)
            .then(time2= > {
                return step2(time1, time2)
                    .then(time3= > [time1, time2, time3])
            })
            .then(times= > {
                const [time1, time2, time3] = times
                return step3(time1, time2, time3)
            })
            .then(result= > {
                console.log(`result is ${result}`)
            })
    }
    doIt()
    Copy the code

    Do you feel a little complicated? That bunch of parameter processing is the death of the Promise scheme – parameter passing is too troublesome to watch dizzy!

    Then implement with async/await:

    async function doIt() {
        console.time("doIt")
        const time1 = 300
        const time2 = await step1(time1)
        const time3 = await step2(time2)
        const result = await step3(time3)
        console.log(`result is ${result}`)}Copy the code

    Don’t you feel better?

Relative to the Generator

See the Generator, Promise, async code for asynchronous programming above

  • The method of Generator solves some problems of Promise, and the process is more intuitive and semantic.

  • */yield and async/await already look very similar in that they both provide the ability to suspend execution.

However, async functions have four improvements over Generator:

  • Built-in actuator.GeneratorThe execution of a function must depend on the executor, andasyncThe function has its own executor, which is called in the same way as ordinary functions
  • Better semantics.asyncawaitIn contrast to*yieldMore semantic
  • Wider applicability.coModule convention,yieldCommand can only be followed byThunkA function orPromiseObject. whileasyncFunction of theawaitThe command can be followed by a Promise or a value of primitive type (Number, string, Boolean, but this is equivalent to a synchronous operation)
  • The return value is Promise.asyncThe return value of the function isPromiseObject,GeneratorFunction returnedIteratorObject is convenient and can be used directlythen()Method is called.

The point here is that the built-in actuator encapsulates all the extra work we need to do (write actuator/dependency CO module) internally.

Write asynchronous logic with synchronous thinking

The biggest advantage of async/await is that we can write asynchronous business logic with synchronous thinking, so the code as a whole looks easier to read.

The execution order of async/await

With the js event loop, let’s look at the execution order of async/await:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')}async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')},0)

new Promise(resolve= > {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')})console.log('script end')

/*
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
*/
Copy the code

Analyze:

  1. Execute code, outputscript start.
  2. Execute async1, which in turn calls async2, and printasync2 end. Back to async1, await is encountered, let the thread go, and the following code is put into the microtask queue.
  3. When setTimeout is encountered, a macro task is generated
  4. Execute the Promise, outputPromise. When then is encountered, the first microtask is generated
  5. Continue to execute the code, outputscript end
  6. After the code logic is executed (the current macro task is executed), the microtask queue generated by the current macro task is executed and the second task thrown to the microtask queue is outputasync1 end.
  7. Execute the task thrown to the microtask queue in step 4, outputpromise1, which generates a microtask and is added later.
  8. Perform the resulting microtask, outputpromise2, the current microtask queue completes.
  9. Finally, execute the next macro task, setTimeout, outputsetTimeout

3 points of async/await attention

  1. Error handling in async/await
  2. Watch out for await blocking
  3. Using await forEach

Let’s go into more details.

Error handling in async/await

Let’s start with the following example:

let a
async function f() {
    await Promise.reject('error')
    a = await 1 // This "await" is not executed
}
f().then(v= > console.log(a))
Copy the code

In the above code, if an async function is await, reject, the rest will not be executed.

Async receives the returned value and determines success if it is not an exception or reject. The async function can return values of various data types, false,NaN,undefined… All in all, resolve

Async fails reject by returning the following result

  1. Contains variables or functions that are directly used and not declared.
  2. An internal error was thrownthrow new ErrorOr returnsrejectstateReturn promise.reject (' execute failed ')
  3. Function method execution error (🌰 : Object using push()), etc…

I want to await and reject the above code, but I still need the following code to execute. What should I do?

  1. Use try-catch to do error catching

    let a
    async function correct() {
        try {
            await Promise.reject('error')}catch (error) {
            console.log(error)
        }
        a = await 1
        return a
    }
    
    correct().then(v= > console.log(a)) / / 1
    Copy the code
  2. Use the promise catch to do error catching

    let a
    async function correct() {
        await Promise.reject('error').catch((err) = > {
            console.log(err)
        })
        a = await 1
        return a
    }
    
    correct().then(v= > console.log(a)) / / 1
    Copy the code
  3. A lazier and more advanced approach: use a Webpack loader to automatically inject try/catch code.

Watch out for await blocking

Since await blocks async functions, the code looks more like synchronous code and is easier to read and understand.

Beware of await blocking, however, because some blocking is unnecessary and improper use can affect the performance of your code.

Look at the wrong chestnut:

async function Fn() {
    let a = await ajax(urla)
    let b = await ajax(urlb)
    console.log(a + b)
}
Copy the code

The code above is want to ask for return the value of joining together two interfaces looks like it’s not a problem, but: request a interface, clog Fn method, b interface would not have to request, to a interface return data, such as the macro tasks are performed, will be asked to come back after the data assigned to a, b interface to request. This is equal to maintaining only one request per asynchronous HTTP request thread.

This seriously affects the performance. Maybe when I request interface B, THERE is nothing for JS to execute and I have to wait for interface B to return the data. Asynchronous HTTP request threads can easily maintain multiple requests.

So we can write this:

async function Fn() {
    let aPromise = ajax(urla)
    let bPromise = ajax(urlb)
    let a = await aPromise
    let b = await bPromise
    console.log(a + b)
}

Copy the code

In this way, request A and request B can be requested at the same time, improving performance.

Of course, if you are familiar with promises, you can use the promise. all method directly, or await after promise. all will not be expanded here.

async function Fn() {
    Promise.all([ajax(urla), ajax(urlb)]).then((values) = > {
      console.log(values) // [a, b]})}Copy the code

Using await forEach

The problem

For asynchronous code, forEach does not guarantee sequential execution.

Here’s an example:

Three requests, in order to loop through the data:

const urls = [
  'https://1'.'https://2'.'https://3'
]

async function test() {
  await urls.forEach(async item => { 
    const res = await ajax(item)
    console.log(res)
  })
  
  console.log('the end')
}

test()
Copy the code

The expected results are:

1
2
3The end of theCopy the code

But it might actually print:

I start by printing out the end

The end of the2
1
3
Copy the code

why

Why is that? I think it’s worth taking a look at the underlying implementation of forEach.

// Core logic
for (var i = 0; i < length; i++) {
  if (i in array) {
    var element = array[i]
    callback(element, i, array)
  }
}
Copy the code

As you can see, forEach takes it and executes it directly, which makes it unable to guarantee the order in which the asynchronous tasks are executed.

For the first, an async, await method is created internally, await blocks the following code, giving up the thread of the current async method, and for the second.

When the forEach function completes, it creates three methods. For example, the asynchronous HTTP thread will call back to the end of the macro task queue if the subsequent task is short. So execute first.

So does the map method.

The solution

How to solve this problem?

It’s actually quite simple,

  1. We use regular for loops or for… Of can be easily solved. This method is serial, that is, sending one request to get the result, then sending the next request.

    Use for… A major disadvantage of the OF loop is that it does not perform well compared to other loop options in Javascript. However, performance parameters can be ignored when used for await asynchronous calls.

    const urls = [
      'https://1'.'https://2'.'https://3'
    ]
    
    async function test() {
      for(const item of urls) {
    	const res = await ajax(item)
    	console.log(res)
      }
      
      console.log('the end')
    }
    
    test()
    Copy the code
  2. You can use the map method to return a promise array corresponding to the URL array. Then use promise.all. This approach is parallel.

    The promise.all method simply sends all requests at once, in parallel mode, and waits for all the results to be received before printing them out in sequence. Instead of sending one request in order, it receives the result and then sends the next request.

    const urls = [
      'https://1'.'https://2'.'https://3'
    ]
    
    async function test() {
      let a = urls.map(item= > ajax(item))
      
      console.log(await Promise.all(a))
      
      console.log('the end')
    }
    
    test()
    Copy the code

for… Of solution principle — Iterator

This seems like a no-brainer, but have you ever wondered why it works?

We all know: for… Instead of traversing through execution in a crude way like forEach, of uses a special means of traversing — iterators.

First, it is an iterable data type for arrays. So what are iterable data types?

The data type native to the [symbol. iterator] property is an iterable data type. Such as groups, class arrays (e.g. Arguments, NodeList), sets, and maps.

Iterables can be traversed by iterators.

const urls = [
  'https://1'.'https://2'.'https://3'
]
// This is the iterator
let iterator = urls[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())


// {value: 'https://1', done: false}
// {value: 'https://2', done: false}
// {value: 'https://3', done: false}
// {value: undefined, done: true}
Copy the code

Therefore, our code can be organized like this:

async function test() {
  const urls = [
      'https://1'.'https://2'.'https://3'
  ]
  // This is the iterator
  let iterator = urls[Symbol.iterator]()
  let res = iterator.next()
  while(! res.done) {let value = res.value
    console.log(value)
    await ajax(value)
    res = iterater.next()
  }
  console.log('the end')}/ / 1
/ / 2
/ / 3
/ / end
Copy the code

Multiple tasks successfully executed in sequence! Actually, for… The of loop code is the syntactic sugar of this code.

Relearning generators

Go back to the code for iterator to traverse the array of urls.

Yi? The return value has value and done attributes, and the generator can also call next, which returns the same data structure. !

Yes, the generator itself is an iterator.

Since it is an iterator, it can use for… Did you go through “of”?

Of course. I’m not going to expand it here.

reference

  1. JS asynchronous programming six schemes
  2. Several pits in async-await array loops
  3. Understand the async/await
  4. Once you know async/await, solve callback hell
  5. Detail the advantages of async/await over Promise
  6. Why use async/await?
  7. Understand the async/await