The single thread is used

Js language is designed to run in the browser at the beginning, the purpose is to achieve interaction on the page. Dom manipulation is at the heart of page interaction, which means it must use a single-threaded model, or else complex thread synchronization problems will occur. If multiple threads work together when JS is running, and one thread modifies a DOM element and another thread modifies or deletes that element, the browser cannot determine which thread to use. Therefore, the execution environment of THE JS language is single threaded.

Advantages and disadvantages of single threading

The biggest advantage of single threading is that it’s simpler and safer

The disadvantage of single-threading is also obvious: if there is a particularly time-consuming task in the process of code execution, the rest of the code (task) has to wait for the previous task to complete before it can execute, giving the user a sense of suspended animation.

In order to solve the disadvantage of single thread, JS has two modes of task execution: 1. Synchronous mode 2. Asynchronous mode

Before we dive into both modes, let’s take a look at what asynchronous JS programming involves.

An overview of asynchronous programming content

  • Synchronous and asynchronous modes
  • Event loops and message queues (how JS implements asynchronous patterns)
  • Several ways of asynchronous programming
  • Promise asynchronous schemes, macro/micro task queues
  • Generator asynchronous scheme, Async/Await syntax sugar

Synchronous and asynchronous modes

Synchronous mode

Synchronous mode: code tasks are executed sequentially, and the latter task must wait for the previous task to finish before starting. Programs are executed in the same order as code is written, and in single-threaded mode, most tasks are executed in synchronous mode.

Asynchronous mode

Asynchronous mode: Each task has one or more callback functions. After the completion of the previous task, the callback function will not be executed before the completion of the latter task. Therefore, the execution order of the program is inconsistent with the order of the tasks.

Asynchronous mode For developers, the biggest difficulty of single-threaded asynchronous mode is the confusion of code execution order. It is very important for the JS language, without which you cannot handle a large number of time-consuming tasks simultaneously.

console.log('print begin')
setTimeout(function timer1 () {
    console.log('timer1 invoked')},1800)
setTimeout(function timer2 () {
    console.log('timer2 invoked')
    setTimeout(function inner () {
        console.log('inner invoked')},1000)},1000)
console.log('print end')
// print begin
// print end
// timer2 invoked
// timer1 invoked
// inner invoked
Copy the code

At some point, the JS thread initiates an asynchronous call, which then continues to perform other tasks. At this point, the asynchronous thread will execute the asynchronous task independently, and after execution, the callback will be put into the message queue. After execution, the MAIN JS thread will execute the tasks in the message queue. It is important to note that JS is single-threaded, browsers are multi-threaded, and some API execution is performed by a separate thread initiated by the browser.

By synchronous and asynchronous, I don’t mean the way code is written, but the apis provided by the runtime environment (browser environment or Node environment) work in synchronous or asynchronous mode.

The callback function

Callbacks are the most basic method of asynchronous programming.

The advantages of callback functions are that they are simple, easy to understand, and deploy. The disadvantages are that they are not conducive to code reading and maintenance, and that they are highly Coupling between various parts, making the process confusing.

function foo(callback) {
    setTimeout(function(){
        callback()
    }, 1000)
}

foo(function() {
    console.log('This is a callback function.')})Copy the code

There are other ways to implement asynchrony, such as event listening and publish-subscribe, which are also variations on the callback function.

Promise

Summary of Promise

Callbacks, though, are the foundation of all asynchronous programming schemes. However, if we directly use traditional callbacks to complete complex asynchronous processes, we cannot avoid a large number of nested callback functions. Causes the problem of callback hell.

To avoid this problem. The CommonJS community came up with the Promise specification, called the language specification in ES6.

A Promise is an object that expresses whether an asynchronous task succeeds or fails after execution.

Promise basic usage

Return to resolve

const promise = new Promise((resolve, reject) = > {
  resolve(100)
})

promise.then((value) = > {
  console.log('resolved', value) // resolve 100
},(error) = > {
  console.log('rejected', error)
})
Copy the code

Return to reject

const promise = new Promise((resolve, reject) = > {
  reject(new Error('promise rejected'))
})

promise.then((value) = > {
  console.log('resolved', value)
},(error) = > {
  console.log('rejected', error)
  // rejected Error: promise rejected
  // at E:\professer\lagou\Promise\promise-example.js:4:10
  // at new Promise (
      
       )
      
})
Copy the code

Even if there are no asynchronous operations in the Promise, the then method’s callback function is still queued up in the event queue.

Promise case

Use Promise to encapsulate an Ajax case

function ajax (url) {
  return new Promise((resolve, rejects) = > {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    // New events provided in HTML5 will not be executed until the request completes (readyState is 4)
    xhr.onload = () = > {
      if(this.status === 200) {
        resolve(this.response)
      } else {
        rejects(new Error(this.statusText))
      }
    }
    // Start executing the asynchronous request
    xhr.send()
  })
}

ajax('/api/user.json').then((res) = > {
  console.log(res)
}, (error) = > {
  console.log(error)
})
Copy the code

The essence of the Promise

The callback function is essentially used to define the tasks that need to be performed after the asynchronous task ends. The callback function here is passed through the THEN method

Promise chain call

Common misconceptions about

  • Nested use is one of the most common pitfalls of using promises. The method to use promise’s chain calls is as flat as possible for the asynchronous task.

Understanding of chain calls

  • The Promise object then method returns a brand new Promise object. We can continue calling the then method, and if the return is not a Promise object but a value, that value is passed as the value of resolve. If there is no value, the default is undefined
  • The subsequent THEN method registers the callback for the Promise returned by the previous THEN
  • The return value of the callback function from the previous THEN method is used as an argument to the callback from the later THEN method
  • If a Promise is returned in the callback, then callbacks to subsequent then methods wait for it to end

Promise exception handling

The onRejected method of the callback in then

.catch() (recommend)

If there is an exception in a promise, you call reject, which can also be used. Catch ()

Using the.catch method is more common because it is more consistent with chain calls

ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  }).catch(function onRejected(error) {
    console.log('onRejected', error)
  })
  
/ / equivalent to
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .then(undefined.function onRejected(error) {
    console.log('onRejected', error)
  })
Copy the code

The catch form is the same as the form of the second argument in the previous then.

  • .catch() is used to process the previous promise returned by.then(), but the first promise is extended to the catch, whereas THRN’s second argument only catches the first promise. If an error is reported in the current then resolve function processing, it will not be caught.

So. Catch is a failed callback that registers the entire promise chain. It is recommended to use

Unhandledrejection event on a global object

It is also possible to register an unHandledrejection event on a global object to handle promise exceptions that have not been caught manually in the code, and of course this is not recommended.

It makes more sense to explicitly catch every possible exception in your code, rather than leaving it to global handling

// Browser environment
window.addEventListener('unhandledrejection'.event= > {
  // Reason => The reason for the failure, usually an error object
  // PROMISE => Abnormal promise object
  const { reason, promise } = event
  console.log(reason, promise)
  event.preventDefault()
}, false)

/ / the node environment
process.on('unhandledRejection'.(reason, promise) = > {
  // Reason => The reason for the failure, usually an error object
  // PROMISE => Abnormal promise object
  console.log(reason, promise)
})
Copy the code

Let’s look at coroutines before we talk about Generator

coroutines

Traditional programming languages have long had solutions for asynchronous programming (actually multitasking). One of these is called a coroutine, which means that multiple threads work together to complete asynchronous tasks.

Coroutines are a little bit like functions and a little bit like threads. Its operation process is roughly as follows:

  • Step one, coroutine A starts executing.
  • In the second step, coroutine A is paused halfway through execution, and execution authority is transferred to coroutine B.
  • In the third step, the coroutine B returns execution authority.
  • Step 4, coroutine A resumes execution.

The coroutine A of the above process is an asynchronous task because it is executed in two (or more) segments.

For example, the coroutine for reading a file is written as follows.

function asnycJob() {
  // ...
  var f = yield readFile(fileA);
  // ...
}
Copy the code

The asyncJob function in the above code is a coroutine whose secret is the yield command. It says execution at this point, execution will be handed over to some other coroutine. That is, the yield command is the boundary between the two phases of asynchrony.

The coroutine pauses at yield, returns to execution, and continues from where it was suspended. Its biggest advantage is that the code is written very much like a synchronous operation.

Generator

The use of Generator functions

Generator functions are an ES6 implementation of coroutines with the best feature of surrendering execution of the function (i.e., suspending execution). The following is a Generator function that, unlike normal functions, can be paused, so the function name is preceded by an asterisk. The whole Generator function is a encapsulated asynchronous task, or a container for asynchronous tasks. Where asynchronous operations need to be paused, use the yield statement.

function * foo() {
    console.log('invoke start')
    const value = yield 'foo'
    console.log(value)
}
const g = foo()
g.next(); // invoke start {value: "foo", done: false}
g.next('end'); // end {value: undefined, done: true}
Copy the code

In the code above, calling the Generator returns an internal pointer (traverser) g. This is another way in which Generator functions differ from normal functions in that executing them does not return a result, but rather a pointer object. Calling the next method on pointer G moves the internal pointer to the first yield statement encountered, as in ‘foo’. The parameters passed to the next method are returned as the result of the asynchronous task in the previous phase (the return value of the yield statement).

In other words, the next method performs the Generator function in stages. Each call to the next method returns an object representing information about the current stage (value and Done attributes). The value attribute is the value of the expression following the yield statement and represents the value of the current phase; The done attribute is a Boolean value indicating whether the Generator has completed execution, that is, whether there is another phase.

Generator function error handling

Error handling code can be deployed inside the Generator functions to catch errors thrown outside the function.

function * gen(x) {
    let y
    try {
        y = yield x + 2
    } catch(e) {
        console.log(e)
    }
    return y
}
const g = gen(1)
g.next();
g.throw('error') // error
Copy the code

The last line of the code above, using the throw method of the pointer g, can be thrown by the try… Catch code block capture. This means that the code that fails is separated in time and space from the code that handles the error, which is certainly important for asynchronous programming.

The Generator functions perform asynchronous tasks

The following example uses nesting to show the evolution of the execution of a generator function

function * main() {
    yield ajax('/api/user.json')
    yield ajax('/api/article.json')
    yield ajax('/api/book.json')}const g = main()
const result = g.next()

result.value.then(data= > { // This code can be rewritten recursively
    const result2 = g.next(data)
    if(result2.done) { return }
    result2.value.then(data= > {
        const result3 = g.next(data)
        if(result3.done) { return }
        result3.value.then(data= > {
            g.next(data)
            / /...})})})Copy the code

The code above can be wrapped using recursion

function * request() {
    yield ajax('/api/user.json')
    yield ajax('/api/article.json')
    yield ajax('/api/book.json')}const g = request()
function handleResult(result) { // Encapsulate this as a generator function executor
    if(result.done) { return }
    result.value.then(data= > {
        handleResult(g.next(data))
    })
}
handleResult(g.next())
Copy the code

Further encapsulate the above code as a generator function executor

function * request() {
    yield ajax('/api/user.json')
    yield ajax('/api/article.json')
    yield ajax('/api/book.json')}function co(generator) {
    const g = generator()
    function handleResult(result) {
        if(result.done) { return }
        result.value.then(data= > {
            handleResult(g.next(data))
        })
    }
    handleResult(g.next())
}
co(request)
Copy the code

Add promise failure processing logic to improve

function * request() {
    try {
        yield ajax('/api/user.json')
        yield ajax('/api/article.json')
        yield ajax('/api/book.json')}catch(error) {
      	console.log(error)
    }
}

function co(generator) {
    const g = generator()
    function handleResult(result) {
        if(result.done) { return }
        result.value.then(data= > {
            handleResult(g.next(data))
        }, error= > {
            g.throw(error)
        })
    }
    handleResult(g.next())
}
co(request)
Copy the code

The CO library saves you from writing actuators for Generator functions.

In the code above, the Generator function is automatically executed as soon as the CO function is passed in.

In fact, the CO library is a small tool published in June 2013 by TJ Holowaychuk, a well-known programmer, for automatic execution of Generator functions.

Let’s see how you can perform a real asynchronous task using the Generator function (without nesting and without the co function).

const fetch = require('node-fetch');

function * gen() {
    const url = 'https://api.github.com/users/github';
    const result = yield fetch(url);
}

const g = gen();
const result = g.next();

result.value.then(function(data) {
    return data.json();
}).then(function(data){
    g.next(data);
});
Copy the code

async/await

In short, async functions are syntactic sugar for Generator functions.

There is a Generator function that reads multiple JSON data in sequence.

function * request() {
    yield ajax('/api/user.json')
    yield ajax('/api/article.json')
    yield ajax('/api/book.json')}function co(generator) {
    const g = generator()
    function handleResult(result) {
        if(result.done) { return }
        result.value.then(data= > {
            handleResult(g.next(data))
        })
    }
    handleResult(g.next())
}
co(request)
Copy the code

Using async/await to rewrite

async function request() {
    await ajax('/api/user.json')
    await ajax('/api/article.json')
    await ajax('/api/book.json')}const promise = request() // Async calls return promise objects for overall control
promisethen(data= > {
  	console.log('Take overall control')})Copy the code

A comparison shows that an async function simply replaces the asterisk (*) of a Generator function with async, yields with await, and nothing more.

Advantages of async functions

The improvements of async over Generator are as follows.

(1) Built-in actuators. Generator functions must be executed by an executor, hence the CO library, whereas async functions come with an executor. In other words, async functions are executed exactly like normal functions, with only one line.

(2) Better semantics. Async and await are semantic clearer than asterisks and yield. Async means that there is an asynchronous operation in a function, and await means that the following expression needs to wait for the result.

(3) wider applicability. According to the co library convention, yield can be followed only by Thunk or Promise, and await can be followed by Promise and primitive values (numeric, string, and Boolean, but this is equivalent to synchronous operations).

Recommended reading

  • The first bullet in JavaScript functional programming – higher-order functions, pure functions, currization
  • The second bullet in JavaScript Functional Programming — Function Composition (Composition)

reference

  • Ruan Yifeng’s network log