Description of content

It is well known that the current mainstream javascript environment is a single threaded mode to execute javascript code.

The reason for working in single-threaded mode

The reason for single-threaded work has to do with its original purpose. The original javascript language was a scripting language that ran on the browser side, and the purpose was to achieve dynamic interaction on the page, and the core of dynamic interaction was Dom operation. This leads to the need to use a single threaded model or have very complex thread synchronization issues. If we have multiple threads working at the same time, one thread modiifies a Dom and another thread deletes a Dom, then the browser has no way of knowing which thread will take the result. To avoid these problems, javascript is designed to work in single-threaded mode from the start.

  • Single-threaded: Single-threaded refers to the js execution environment in which only one environment is responsible for executing code.
  • Pros: Safer and simpler
  • Disadvantages: Time-consuming tasks block execution

To solve the blocking problem of time-consuming tasks, javascript divides tasks into two modes of execution. (Synchronous mode, asynchronous mode). The focus of this article is on the content that comes first in javascript versus asynchronous mode. It mainly includes the following:

  • Synchronous vs. Asynchronous mode
  • Event loops and message queues
  • Several ways of asynchronous programming
  • Promise Asynchronous solutions, macro tasks, microtask queues
  • Generator asynchronous scheme, Async/await syntax sugar

Synchronous mode

Synchronous mode means that tasks in our code are executed in sequence, and the next task has to wait for the last task to finish before it can start executing. The execution order is exactly the same as the encoding order of our code.

  • Synchronization does not mean simultaneous execution, but queuing

We can demonstrate this in code:

console.log("global begin");

function bar() {
  console.log("I am foo");
}

function foo() {
  console.log("I am the bar");
  bar();
}
foo();
console.log("global end");

// Execution result
// global begin
/ / I am a foo
/ / I'm a bar

Copy the code

Enforcement mechanism:

  • The JS execution engine loads the entire code, pushes an anonymous call onto the call stack, and then starts executing line by line

  • So I’m going to see the first rowconsole.log('global begin')If you call it, it willconsole.log('global begin')Push it on our call stack, and the console printsglobal beginWhen the execution is complete, pop the stack and continue the execution

  • This is followed by two function declarations, neither function nor variable declarations will result in calls, and the execution continues.

  • Next up is the call to foo(), pressing the stack as in the first line

  • When you encounter console.log(” I am foo”) inside the bar function, you press the stack, the console prints “I am foo”, and then click the stack.

  • Next, the bar() function call is encountered, and the stack is pressed first

  • Inside the foo function, we encounter console.log(” I am bar”), press the stack, the console prints I am bar, and bounces the stack

  • After the bar function is executed, the stack is clicked

  • Then the bar function is executed and the stack is clicked

  • If you encounter console.log(“global end”), push the stack, and the console prints “global End “, then pop the stack

  • When the entire code is executed, our code will be cleared



If a task or a row takes too long to execute, it will block, and the page will stall or stall for the user, so asynchronous operations are necessary to resolve the inevitable time-consuming operations.

Asynchronous mode

  • Browser side Ajax operations
  • Large file reads and writes in nodeJs
  • Scheduled tasks such as setTimeout

introduce

  • In asynchronous mode, the API does not wait for the current task to finish before executing the next task. For time-consuming operations, the API executes the next task as soon as it is enabled
  • The subsequent logic of a time-consuming task is defined in the form of a callback function, which is called after the asynchronous task is finished.
  • Asynchronous mode is so important to JavaScript that without asynchronous mode, single-threaded JavaScript syntax cannot handle a large number of time-consuming tasks simultaneously
  • To the developer, code in asynchronous mode is executed in leaps and bounds and is not as straightforward as synchronized code.

Let’s look at some code to analyze how the asynchronous mode is executed:

console.log("global begin");
setTimeout(function timer1() {
  console.log("timer1 invoke");
}, 1800);
setTimeout(function timer2() {
  console.log("timer2 invoke");
  setTimeout(function inner() {
    console.log("inner invoke");
  }, 1000);
}, 1000);
console.log("global end");
Copy the code
  • It also starts by loading the overall code, and then pressing the global anonymous call
  • thenconsole.log("global begin")Press the stack, console printglobal begin.console.log("global begin")Out of the stack.
  • Then push thesetTimeout(timer1)This function is called asynchronously internally, so we need to be concerned about what the internal API is doing. Obviously, internally, we started a timer for time1, and then set it aside (this timer works independently and is not affected by JS single threading)setTimeoutI’m done with the call so I’m going to bounce the stack and continue.
  • Then I met anothersetTimeoutCall, the same thing first push the stack, and then open another countdown timer, and then bounce the stack
  • Come across againconsole.log("global end")Push the stack, console print, then bounce the stack
  • Print out the final oneglobal endThen the anonymous call to the whole is executed, and the call stack is cleared
  • This is where the Event Loop comes into play, listening to the call stack and the time queue (once the call stack is over, the first callback is pulled from the Event queue and introduced into the call stack).
  • The timer2 timer ends, and the timer2 function is placed in the first place in the message queue
  • Then the timer1 timer ends and the timer1 function is placed second on the message queue
  • Once the message queue changes, the event loop will listen and take out the first function (Timer2) of the message queue and push it onto the call stack to continue the execution of Timer2 (equivalent to starting a new round of execution). The execution process is the same as before, and the same is true for asynchronous calls.
  • The call repeats until there are no more tasks in the call stack and message queue, and the overall task is finished.



As shown in the figure, the JavaScript thread initiates an asynchronous call at a certain moment, and then continues to execute other tasks later, while the asynchronous thread will execute the asynchronous task. After the asynchronous task is completed, it will put the asynchronous task into the message queue. After the JS thread completes all tasks, it will execute the tasks in the message queue in turn (note: JavaScript is single-threaded and browsers are not single-threaded, or more specifically, some of the internal apis that are called through JavaScript are not single-threaded, such as the countdown timer above, which has a separate thread inside).



Asynchronous mode is very important for single-threaded JavaScript, and it’s a core feature of JavaScript. Also, because of the asynchronous MODE API, the code is not easy to read, and the execution order is much more complicated, so there’s a lot of syntax for asynchronous. In particular, there are a number of new syntax, new features that are exiting after ES2015, that are slowly making up for the lack of JavaScript.

The callback function

The callback function is the fundamental way to realize asynchronous programming, in fact all the root of the asynchronous solution is a callback function, the callback function can be understood as know what to do, but do not know when the things depend on the task of finish, so the best way is to write these things steps of a function (the callback function), to the executor of the task, The executor knows when to finish, and then executes what you want to do (the callback function). With ajax as the example, ajax is hope to get the data to do some processing, but the request when completed I don’t know, so put after the request and response to execute unified function, ajax performed automatically perform this function, the defined by the caller, to the function to be executed by the practitioner is known as the callback function, The implementation is as simple as passing functions as arguments, but it’s not easy to read and the execution sequence is messy.

Promise

Callbacks are the foundation of JavaScript asynchronous programming, but using traditional callbacks directly to complete complex asynchronous processes can lead to a large number of nested callbacks, which can lead to the commonly referred to as callback hell. To avoid this problem, the CommonJS community proposed the Promise specification. The goal was to provide a more rational and powerful asynchronous programming solution for JavaScript, which was later standardized in ES2015 and became the language specification.

A Promise is an object used to indicate whether an asynchronous task will succeed or fail when it is finally completed. It is like a Promise made internally to the external. Initially, the Promise is pending and eventually it may be Fulfilled or failed. This will be fulfilled gradually, which will be fulfilled no matter whether it is a success or a failure. After the promise is fulfilled, a task will be carried out. And an obvious feature is that once the result is known, it can never be changed.

The basic use

// Promise basic example
// The Promise constructor needs to take a function as an argument, which can be interpreted as the logic of the object's Promise
// This function is executed synchronously during the construction of the Promise
// This function accepts two arguments, resolve/reject, which change the Promise's state to succeed and fail, respectively
// The state is determined, which means only one of the resolve/reject calls will be made

const promise = new Promise(function (resolve, reject) {
  // Make good on a promise

  // Resolve (100

  reject(new Error('promise rejected')) // Promise failed
})

promise.then(function (value) {
  // Even if there is no asynchronous operation, the callbacks passed in the then method are queued for the next round of execution
  console.log('resolved', value)
}, function (error) {
  console.log('rejected', error)
})

console.log('end')

Copy the code

Ajax case

// Promise style AJAX

function ajax(url) {
  return new Promise(function (resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url)
    xhr.responseType = "json";
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  });
}

ajax("/api/get.json").then(
  function (response) {
    console.log(response);
  },
  function (error) {
    console.log(error); });Copy the code

Promise Common mistakes

We can see from the above demo code that a Promise essentially uses a callback function to define the tasks to be executed after the asynchronous task has finished. However, here the callback function is passed in through the then method, and the Promise divides the callbacks into two types (onFulfilled,onReject).



At this point, we thought about it and realized that since it’s still a callback function, if we need to execute multiple asynchronous tasks in series, we still have the problem of nesting the callback function.

  • In the traditional way of thinking we have this situation
// Nesting promises is the most common mistake
ajax('/api/urls.json').then(function (urls) {
  ajax(urls.users).then(function (users) {
    ajax(urls.users).then(function (users) {
      ajax(urls.users).then(function (users) {
        ajax(urls.users).then(function (users) {})})})})Copy the code
  • The right approach is to keep the asynchronous tasks as flat as possible by taking advantage of the chain-call nature of the Promise then() method

As we said earlier, the then() method adds a state-specific callback to the Promise object. If a Promise object fails, the callback function can be omitted. The main feature of the then method is that it also returns a Promise object internally

var promise = ajax('/api/users.json')
var promise2 = promise.then(
  function onFulfilled (value) {
    console.log('onFulfilled', value)
  },
  function onRejected (error) {
    console.log('onRejected', error)
  }
)

console.log(promise2)
console.log(promise2 === promise) //false
Copy the code

The then method returns a new promise object, which is intended to implement the promise chain. For example:

ajax("/api/users.json")
  .then(function (value) {
    console.log(1111);
  }) // => Promise
  .then(function (value) {
    console.log(2222);
  }) // => Promise
  .then(function (value) {
    console.log(3333);
  }) // => Promise
  .then(function (value) {
    console.log(4444);
  });

/ / 1111
/ / 2222
/ / 3333
/ / 4444
Copy the code

We can also manually return a Promise object inside the THEN method, for example:

ajax('/api/users.json')
  .then(function (value) {
    console.log(1111)
    return ajax('/api/urls.json')})// => Promise
  .then(function (value) {
    console.log(2222)
    console.log(value)
    return ajax('/api/urls.json')})// => Promise
  .then(function (value) {
    console.log(3333)
    return ajax('/api/urls.json')})// => Promise
  .then(function (value) {
    console.log(4444)
    return 'foo'
  }) // => Promise
  .then(function (value) {
    console.log(5555)
    console.log(value)
  })
Copy the code

Conclusion:

  • The THEN method on the Promise object returns an entirely new Promise object
  • The subsequent THEN method registers the callback for the Promise returned by the previous THEN
  • The return value of the previous THEN method callback is taken as an argument to the later THEN method callback
  • If a Promise is returned in the callback, then the callback to the then method waits for it to end

Promise Exception handling



As mentioned earlier, a Promise result is called in the event of a failurethenThe onRejected callback is passed to the promise.for example, if you request an address that does not exist, or if an exception is thrown or thrown manually, the onRejected callback will be executed. Look at this code:


function ajax (url) {
  return new Promise(function (resolve, reject) {
    // An exception occurs during the execution of foo(). The onReject callback is executed
     throw new Error(a)An attempt to throw an exception manually executes the onReject callback
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}

ajax('/api/users11.json')
  .then(function onFulfilled (value) {
    console.log('onFulfilled', value)
  })
  .then(undefined.function onRejected (error) {
    console.log('onRejected', error)
  }) 
Copy the code

A more common way to register an onReject callback is to register the onReject callback with the promise instance’s catch method.

ajax('/api/users11.json')
  .then(function onFulfilled (value) {
    console.log('onFulfilled', value)
  })
  .catch(function onRejected (error) {
    console.log('onRejected', error)
  })
Copy the code

Cath is another version of then, as follows:

// If (onRejected) = if (onRejected)

ajax('/api/users11.json')
  .then(function onFulfilled (value) {
    console.log('onFulfilled', value)
  })
  .then(undefined.function onRejected (error) {
    console.log('onRejected', error)
  })
Copy the code

Looking at the results, we don’t find any difference between the two, but a close comparison reveals a big difference:

  • Because eachthenMethod returns a brand new promise, and the catch we call later in the chain call is actually given to the frontthenThe callback returned by the Promise method is not specified for the first Promise object, but because this is the same Promise chain, the previous Promise will always be passed, so that the exception of the first Promise can be caught and passedthenThe callback function specified by the second argument is just the exception specified for the current Promise

Promise static methods

Promise.resolve

This will be fulfilled if the Promise object foo is returned as the value of this Promise
// That is, the parameter we get in the onFulFilled callback is foo
Promise.resolve('foo') 
  .then(function (value) {
    console.log(value)
  })

// The above code is equivalent to
new Promise(function (resolve,reject){
		resolve('foo')
}).then(function (res){
  console.log(res)//foo
})	

Copy the code

In addition, if promise. resolve receives another Promise object, the Promise object is returned as is. For example:


var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2) //true
Copy the code

Also, if we pass an object that has the same THEN method as a promise, that object can be executed as a promise, for example:

Promise.resolve({
 then:function (onFulFilled,onRejected){
    onFulFilled("foo")
 }
}).then(function (value) {
  console.log(value)
})
Copy the code

Objects with this then method can be said to implement a Thenable interface. The reason for this support is that until native Promises became common, promises were implemented using third-party libraries. If you need to convert third-party Promise objects to original sounds, you can use this mechanism.

Promise. Reject:

Quickly create a Promise object that will fail and use whatever parameter is passed as the reason for the Promise to fail

Parallel execution

Promise.all

If we need to perform multiple asynchronous tasks in parallel, for example with no dependencies between tasks, the best option is to request multiple interfaces at once. The code is as follows:

ajax('/api/users.json')
 ajax('/api/posts.json')
Copy the code

But there will be a problem, how are we to judge all the requests have been the end of the time, the traditional approach is to define a counter, not a request to let it add a success, knew counter and request equals the number of signals the end of all tasks, this method will be very trouble the abnormal situation, also need to consider To do this we can use the Promise’s all method:

var promise = Promise.all([
  ajax('/api/users.json'),
  ajax('/api/posts.json')
])

promise.then(function (values) {
  console.log(values)
}).catch(function (error) {
  console.log(error)
})
Copy the code

The all method receives an array with each element in the array being a Promise object. This method returns a new Promise object. The Promise object will not be returned until all the inner promises have been completed. This array contains the result of each Promise (only if all promises have been executed). If one Promise is rejected then the entire Proimse will fail.

ajax('/api/urls.json')
  .then(value= > {
    const urls = Object.values(value)
    const tasks = urls.map(url= > ajax(url))
    return Promise.all(tasks)
  })
  .then(values= > {
    console.log(values)
  })
Copy the code

Promise.race

Instead of the promise. all method, promise.rece is executed with the first Promise of all tasks completed, as follows:

// Promise.race implements timeout control

const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) = > {
  setTimeout(() = > reject(new Error('timeout')), 500)})Promise.race([
  request,
  timeout
])
.then(value= > {
  console.log(value)
})
.catch(error= > {
  console.log(error)
})
Copy the code

Generator

One of the biggest ways Promise handles asynchronous calls, as opposed to the traditional callback approach, is that the problem of nested callbacks can be solved by chaining calls. But there are still a lot of big callbacks, such as



There is still no way to achieve the readability of traditional synchronized code, which is as follows:



Let’s talk about two better ways to write asynchronous programming

Generator A Generator function

  • When we call a generator function, instead of executing immediately, we get a generator object;
  • This function doesn’t start executing until we call the next method on the generator object;
  • Second, you can use yield to return a value from within a function. In addition, there is a done property in the return value that indicates whether the function is fully executed.
  • Instead of terminating the function like a traditional return, yield simply suspends execution of the function until the next external call to next continues from the yield position.
  • If you pass an argument to the next function, that argument can be received on the left of yield;
  • If called generator. Throw, the generator function throws an exception, and if not caught, the current execution is stopped.
  function * test(){
    console.log('test')
    const req1 = yield 1
    console.log(req1)
    try {
      const req2 = yield 2
      console.log(req2)
    } catch (error) {
      console.log(error)
    }
    yield 3
  }
  // The following will not be executed immediately
  const generator  = test()
  console.log(generator.next())
  // test
  // {value: 1, done: false}
  console.log(generator.next('to A'))
  / / to A
  // {value: 2, done: false}
  generator.throw(new Error('Generator Error'))
  // Error: Generator Error
  // at eval (eval at 
      
        (VM1178:8), 
       
        :17:17)
       
  // at VM1179:18
  // {value: 3, done: false}
Copy the code

Using Generator to manage asynchronous processes:

  function timeOut (id) {
    return new Promise((resolve) = > {
      setTimeout(() = > {
        resolve({ name: "Xiao Ming" + id, age: id, id: id })
      }, 1000)})}function* main () {
    // The first asynchronous function and the processing
    const res1 = yield timeOut(1)
    console.log(res1)
    // The second asynchronous function and processing
    const res2 = yield timeOut(12)
    console.log(res2)
    // The third asynchronous function and processing
    const res3 = yield timeOut(18)
    console.log(res3)
    // The fourth asynchronous function and processing
    const res4 = yield timeOut(24)
    console.log(res4)
  }
  const generator = main()
  const res1 = generator.next()
  res1.value.then(data= > {
    const res2 = generator.next(data)
    res2.value.then(data= > {
      const res3 = generator.next(data)
      res3.value.then(data= > {
        generator.next(data)
      })
    })
  })
  // Output in sequence:
  // {age: 1, id: 1}
  // {id: 1, age: 1}
  // {id: 18}
Copy the code

In the example above, we use the generator function to write the Promise callback at yield, which simplifies the asynchronous problem. The generator function callback can be solved recursively:

  const g1 = main()
  function handleResult (result) {
    if (result.done) {
      return
    }
    result.value.then(data= > {
      handleResult(g1.next(data))
    }, error= >{
      g
    })
  }
  handleResult(g1.next())
  // Output in sequence:
  // {age: 1, id: 1}
  // {id: 1, age: 1}
  // {id: 18}
  // {name: "< =" age: 24, id: 24}
Copy the code

With exception handling, we can wrap an exec function for generator function calls:

function timeOut (id) {
    return new Promise((resolve, reject) = > {
      setTimeout(() = > {
        if (id < 24) {
          resolve({ name: "Xiao Ming" + id, age: id, id: id })
        } else {
          reject(new Error('ID more than 24'}})),1000)})}function* main () {
    try {
      const res1 = yield timeOut(1)
      console.log(res1)
      const res2 = yield timeOut(12)
      console.log(res2)
      const res3 = yield timeOut(18)
      console.log(res3)
      const res4 = yield timeOut(24)
      console.log(res4)
    } catch (error) {
      console.log(error)
    }
  }
  function exec (fun) {
    const generator = fun()
    function handleResult (result) {
      if (result.done) {
        return
      }
      result.value.then(data= > {
        handleResult(generator.next(data))
      }, error= > {
        generator.throw(error)
      })
    }
    handleResult(generator.next())
  }
  exec(main)
  // {age: 1, id: 1}
  // {id: 1, age: 1}
  // {id: 18}
  // Error: the ID exceeds 24
  // at 
      
       :7:16
      
Copy the code

There is a comprehensive library of such generator function effectors in the community: github.com/tj/co this scheme was popular in the past, but has become less popular since ES has async await, but the biggest advantage of using generator schemes is that it flattens our asynchronous calls, which is an important step in JS asynchronous programming.

Async Await

With the Generator, asynchronous programming and synchronous code in JavaScript have a similar experience, but using the Generator requires writing an executor function manually, which is a bit cumbersome. The addition of async await in ES2017 also provides the same flat asynchronous programming experience, and it is more convenient to use as it is standard syntax at the language level. In fact, async await is a more convenient syntax sugar for generator functions. We just change generator functions to normal functions. Use async modifier, yield to await:

  async function main () {
    try {
      const res1 = await timeOut(1)
      console.log(res1)
      const res2 = await timeOut(12)
      console.log(res2)
      const res3 = await timeOut(18)
      console.log(res3)
      const res4 = await timeOut(24)
      console.log(res4)
    } catch (error) {
      console.log(error)
    }
  }
  main()
Copy the code

Async function also has the same effect as Generator. The biggest benefit is that it does not need to cooperate with an actuator such as CO, because it is the standard asynchronous syntax at the language level. Secondly, async will return a Promise object, which is convenient for us to control the code better. Another thing to note is that await can only appear inside functions and cannot be used at the top level scope (it is currently under development and may appear in the standard in the near future).