Read the precursors to this article: JavaScript asynchronous task queues, the Promise principle

Async await serial and parallel

An async function is a function declared using the async keyword. Async functions are instances of AsyncFunction constructors and allow the await keyword. The async and await keywords allow us to write asynchronous behavior based on promises in a more concise way, without having to deliberately chain call promises.

parallel

An example of using async await in parallel

/ / 1
function sleep(delay) {
  return new Promise((resolve) = > {
    setTimeout(resolve, delay);
  });
}

(async() = > {const sleepP1 = sleep(1000);
  const sleepP2 = sleep(2000);
  console.log(sleepP1);
  console.log(sleepP2);
  console.time("sleep");
  await sleepP1;
  await sleepP2;
  console.timeEnd("sleep"); }) ();Copy the code

Think about the final output here.

Promise {<pending>} Promise {<pending>} sleep: 2.003sCopy the code

serial

What if I want you to output more than 3 seconds? What’s the code?

/ / 2; (async() = > {console.time('sleep')
  await sleep(1000)
  await sleep(2000)
  console.timeEnd('sleep')
})()
Copy the code

Here the output is:

Sleep: 3.016 sCopy the code

thinking

Here I have two questions for you, the wise one.

Question 1: “await” is changed to “synchronous”. Why the output in example 1 is still more than 2s?

Question 2: I set 1 and 2 seconds, why the result is a few more milliseconds?

The error of setTimeout in Chrome is about 4ms. As for the error in Node and other browsers, interested readers can check relevant information. One explanation for how this error came about is:

When we execute JS code, we actually put functions or expressions on the execution stack. When we encounter asynchronous code, it is suspended and added to the task queue (divided into macro and micro tasks) when needed. Once the stack is empty, the Event Loop takes the function that needs to be executed from the task queue and executes it on the stack.

As explained above, setTimeout should add the callback function to the (macro) task queue after the set delay and wait for the synchronization and microtask to complete before executing the callback. So it’s no wonder why we have a few more ms.

For question 1, to incorporate a little bit of the Promise principle, I’ve posted some of my handwritten Promise code here

//Promise's constructor
constructor(executor) {
      this.status = PENDING;

      // Attach the values of resolve and reject to the instance object for then and catch access
      this.value = undefined;
      this.reason = undefined;

      // Successfully callback queue
      this.onFulfilledCallbacks = [];
      // Failed callback queue
      this.onRejectedCallbacks = [];

      const resolve = (value) = > {
        // Only pending state can change state
        if (this.status === PENDING) {
          this.status = FULFILLED;
          this.value = value;
          // Execute successful callbacks in sequence
          this.onFulfilledCallbacks.forEach((cb) = > cb(this.value)); }};const reject = (reason) = > {
        if (this.status === PENDING) {
          this.status = REJECTED;
          this.reason = reason;
          // Execute failed callbacks in sequence
          this.onRejectedCallbacks.forEach((cb) = > cb(this.reason)); }};try {
        executor(resolve, reject);
      } catch (err) {
        // The executor executes in error, throws an exception and returns a failed promisereject(err); }}Copy the code

Const sleepP1 = sleep(1000); const sleepP1 = sleep(1000); const sleepP1 = sleep(1000); It does not block subsequent code execution, i.e. the two are in parallel, and after 2s both resolve timer callbacks are added to the macro task queue (the first one was added at 1s). We are actually calling the then of the returned promise when we await it and then getting the value inside. Thus, the code in example 1 executes in parallel, resulting in more than 2s. As for example 2, according to the above analysis, the essence of await is to wait for the promise state to change from PENDING to depressing, and then to take the value in then. Therefore, it is actually a serial, because the value can only be obtained after the resolve execution is completed. My personal understanding is that the await in example 1 only waits for the time for the callback to join the microtask queue and then take it out, while the await in example 2 waits for one more delay set by setTimeout.

Here I am posing a question, if you can solve it quickly then you have a good understanding of async and await serial, parallel and promise.

Question 3: How can the sleep time be 3s plus by adjusting only the code order in Example 1?

Reference answer:

; (async() = > {const sleepP1 = sleep(1000)
  console.log(sleepP1)
  console.time('sleep')
  await sleepP1
  const sleepP2 = sleep(2000)
  console.log(sleepP2)
  await sleepP2
  console.timeEnd('sleep')
})()
Copy the code

Output:

Promise {<pending>} Promise {<pending>} sleep: 3.005sCopy the code

Iterables and generators

Iterable objects are generalizations of arrays. The concept is that any object can be customized to be available in for.. The object used in the of loop.

Iterable protocol

The iterable protocol allows JavaScript objects to define or customize their iterative behavior, such as in a for.. In the of structure, which values can be iterated over. (One saying is that iterables implement the iterator interface for… Of consumption). Some built-in types are also built-in iterables and have the default iterative behavior, such as Array or Map, while others are not (such as Object).

To be an iterable, an object must implement the @@iterator method. This means that the object or its prototype chain must have an @@iterator property whose value is a function whose return value is an iterator protocol-compliant object. Developers can access the property through [symbol.iterator] and rewrite the value of the property.

attribute value
[Symbol.iterator] A function that takes no arguments and returns a matchIterator protocolThe object.

When an object needs to be iterated over (such as putting in a for.. The of loop first calls its @@iterator method with no arguments, and then uses the iterator returned by the method to get the value to iterate over.

It is worth mentioning that when this zero-argument function is called, it is called as a method on an iterable. Therefore, inside a function, the this keyword can be used to access the properties of an iterable to determine what to provide during iteration.

This function can be a normal function or a generator function that returns an iterator object when called. Inside this generator function, yield can be used to supply each entry.

Iterator protocol

The iterator protocol defines a standard way to produce a set of values, whether finite or infinite. When a finite number of values are iterated over, a default return value (undefined) is returned.

An object can be an iterator only if it implements a next() method with the following semantic:

attribute value
next A no-argument function that returns an object that should have the following two properties:doneBoolean if the iterator can produce the next value in the sequencefalse. (This is equivalent to not specifyingdoneThis property. If the iterator has iterated through the sequence, otherwisetrue. In this case,valueIs optional and, if it still exists, is the default return value at the end of the iteration.valueAny JavaScript value returned by the iterator. Done can be omitted when true.next()The method must return an object that should have two attributes:donevalueIf a non-object value is returned (e.gfalseundefined), will throw oneTypeErrorExceptions ("iterator.next() returned a non-object value").

Let’s implement the iterable protocol and iterator protocol for a common object:

const obj = {
  data: [1.2.3.4.5],
  [Symbol.iterator] () {
    const self = this
    let index = 0
    return {
      next () {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false}}else {
          return {
            value: undefined.done: true
          }
        }
      }
    }
  }
}

for (const d of obj) {
  console.log(d)			// Newline outputs 1, 2, 3, 4, 5
}

for (const key in obj) {
  console.log(key)			// Output data only, indicating that the @@iterator attribute is not enumerable
}

Copy the code

It is important to note that implementing an iterative protocol does not necessarily implement an iterable protocol. Here is an example

function makeIterator (array) {
  let index = 0
  return {
    next () {
      return index < array.length
        ? {
            value: array[index++],
            done: false}, {done: true}}}}let iter = makeIterator(['hi'.'hello'])

console.log(iter.next())		//{ value: 'hi', done: false }

//TypeError: iter is not iterable
for (const i of iter) {
  console.log(i)
}
Copy the code

Objects that only implement the iterable protocol but do not implement the iterable protocol are not iterable, as follows:

const iterableObj = {
  data: [1.2],
  [Symbol.iterator]: () = > {
    return {
      data: this.data
    }
  }
}
//TypeError: iterableObj is not iterable
console.log([...iterableObj])
Copy the code

To sum up, an Iterabel object must implement both the iterable and iterator protocols.

MDN considers that the @@iterator method can be called an iterable as long as it is implemented. If the object returned by the method does not meet the iteration protocol, it is called a poorly formed iterable.

Here, personally, I think you can tell if an object is iterable by writing it this way

const isIterable = 可迭代= > {
  try {
    for (const _ of iterable) {
      return true}}catch (error) {
    return false}}console.log(isIterable({ a: 1 }))	//false
console.log(isIterable([1.2]))		//true
Copy the code

Built-in iterables

Currently all built-in iterables are as follows: String, Array, TypedArray, Map, and Set.

API for receiving iterables

  • new Map([iterable])

  • new WeakMap([iterable])

  • new Set(“[iterable])

  • new WeakSet([iterable])

  • new Map([iterable])

  • new WeakMap([iterable])

  • new Set([iterable])

  • new WeakSet([iterable])

  • .

You need the syntax of an iterable

Some statements and expressions require iterables, such as for… Of loops, expansion syntax, yield*, and destruct assignment.

thinking

Question 1: Is a generator object an iterator or an iterable?

Let’s look at a piece of code

const generator = (function * () {
  yield 1
  yield 2
  yield 3}) ()console.log(typeof generator.next) // Output "function", which is an iterator because there is a next method
console.log(typeof generator[Symbol.iterator]) // Print "function", which is an iterable because there is an @@iterator method
console.log(generator[Symbol.iterator]() === generator) // Print true, since the @@iterator method returns itself (that is, an iterator), this is a well-formed iterable

console.log([...generator]) // Output [1, 2, 3]

console.log(Symbol.iterator in generator) //// prints true, because the @@iterator method is an attribute of the aGeneratorObject

Copy the code

To sum up: generator objects are both iterators and iterables:

Finally, let’s look at iterators in the ES6 class

class SimpleClass {
  constructor (data) {
    this.data = data
  }

  [Symbol.iterator] () {
    // Use a new index for each iterator to make the object safe for multiple iterations. For example, using break or nested loops on an object
    let index = 0

    return {
      next: () = > {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false}}else {
          return { done: true }
        }
      }
    }
  }
}

const simple = new SimpleClass([1.2.3.4.5])

for (const val of simple) {
  console.log(val) //'1' 2' 3' 4' 5'
}

Copy the code

Why Generator

One conclusion first: async await is essentially a syntactic sugar of Generator and yield.

Prior to ES6, any function in JS, once started, would run to the end without any other code interrupting it and inserting it. The introduction of generator functions breaks this situation.

Consider the following example:

var x = 1
function foo () {
  x++
  bar()
  console.log('x:', x)
}
function bar () {
  x++
}
foo() //x:3
Copy the code

In this case, we can make sure that bar() runs between x++ and console.log(x). But what if bar() isn’t there? It’s obviously going to be 2 instead of 3.

Imagine if bar() wasn’t there, but for some reason it still ran between X ++ and console.log(x), and x still changed to 3.

Those of you who have learned a preemptive multithreaded language like Java may easily achieve this effect. Unfortunately, JS is not preemptive, nor (yet) multithreaded. However, if Foo () itself can indicate a pause at this point in the code in some way, then such a break (concurrency) can still be implemented in a cooperative manner.

Look at the following code:

var x = 1
function * foo () {
  x++
  yield / / pause
  console.log('x:', x)
}
function bar () {
  x++
}
// Construct an iterator to control the generator
const it = foo()

/ / start foo ()
it.next()
console.log(x) / / 2
bar()
console.log(x) / / 3
it.next() //x:3
Copy the code

Here’s how it works:

  1. it=foo()The operation was not performed*foo()The code in “, just constructs an iterator
  2. The first oneit.next()The generator is started and the code executes until the first one is encounteredyield
  3. *foo()inyieldStatement out pause, execute to this line, the first oneit.next()The call ends. At this time*foo()Running and still running and active, but suspended.
  4. So x is equal to 2
  5. We callbar(), the implementation ofx++
  6. So x is equal to 3
  7. The last of theit.next()The call resumes the generator from the pause*foo()Execute and runconsole.log(x)

Thus, generators are a special class of functions that call them and return a generator that can be started or stopped through an iterator. There is a saying that generators can implement JS coroutines (also known as pseudo-multithreading).

Message passing for the generator

Simply being able to start or stop a program is far from fulfilling the purpose of a coroutine, so the yield keyword is provided in generators to pass values from inside the function, outside the function, via the next(…) of the iterator it generates. Pass the value into the function.

Consideration:

function * foo (x) {
  var y = x * (yield)
  return y
}

const it = foo(6)

it.next()

const res = it.next(7)

console.log(res.value)	/ / 42
Copy the code

As usual, explain the above code execution:

  1. callfoo(6)Produces an iterator that assigns toitPS: Since generators are special functions, they also have inputs and outputs, parameters and return values.
  2. it.next()Start the generator and execution beginsy=x...But then I met oneyieldExpression, which suspends execution and expects the callyieldThe expression provides a result value. Next, callit.next(7), this returns the 7 as the pausedyieldThe result of the expression,
  3. Now the assignment statement is essentiallyvar y = 6*7.return yWill return 42 as the callit.next()Results. In fact hereit.next(7)The return value of{value:42,done:true}Through thereturnOf the object taken outdoneAttributes aretrue.

Note that there is something confusing here: yield and next(…). The call does not match. In general, you need next(…) The call is one more than the yield expression, the next next(…) The parameters passed are passed to the generator function as the return value of the previous yield expression.

Why is there a mismatch?

Because the first next(…) Always start a generator and run to the first yield. Then, the second next(…) The call completes the first paused yield expression and the third next(…). The second yield is invoked, and so on, until the function return or end is encountered.

In fact, just consider the generator code:

var y = x * (yield)
return y
Copy the code

The first yield basically asks the question, “What value should I insert here?”

Who will answer? The first next() has already run, causing the generator to start and run here, so obviously it can’t answer the question.

See the mismatch — the second next(…) For the first yield?

We said earlier that yield can pass values out of the generator, so let’s modify the code above:

function * foo (x) {
  var y = x * (yield 1)
  return y
}

const it = foo(6)

let res = it.next()

console.log(res.value) / / 1

res = it.next(7)

console.log(res.value) / / 42
Copy the code

As you can see, messages are passed in both directions, and a generator function can pass values to the outside, and the outside can pass values to the inside of a generator function. In that case, why don’t we look at the mismatches from the iterator’s point of view?

The first next() call basically asks a question: “Generator *foo(…) What’s the next value to give me?” Who’s going to answer that question? The first yield 1 expression.

See? There is no mismatch here.

So, in effect, yield and next(…), depending on the Angle you are at. Either there is a mismatch or there is none.

However, there is still a next(…) statement as opposed to the yield statement. . So the last it.next(7) again raises the question: What is the next value that the generator will produce? However, there is no yield statement to answer this question anymore, so who does?

Return statement

One interpretation is that yield is equivalent to a special return, because a function return can either pass a value or transfer control to the program. The only difference is that you cannot externally change the value of a return statement.

Generator handling asynchrony

Let’s rewrite the classic fs.readfile (path,cb) in NodeJS with Generator

const fs = require('fs')

function readFile (file) {
  fs.readFile(file, (err, data) = > {
    if (err) {
      it.throw(err)
    } else {
      it.next(data.toString())
    }
  })
}

function * main () {
  try {
    const text = yield readFile('./test.txt')
    console.log(text)
  } catch (err) {
    console.log(err)
  }
}

const it = main()
it.next()

Copy the code

Results:

$ node readFileGenerator.js 
Generator is reall NB!
Copy the code

It’s worth mentioning here that generators generate iterators with an extra throw(…) than normal iterators. Method to handle exceptions.

If you look at the code above, does it look familiar? Does main() look a lot like async? Is this use of yield similar to using await expressions in async functions? Yes, an async function is essentially a generator with an executor, and await is essentially yield, but the asynchronous task followed by await must be a promise. Let’s hand lift an Async actuator.

const getData = () = >
  new Promise(resolve= > {
    setTimeout(() = > {
      const num = Math.floor(Math.random() * 520)
      resolve(num)
    }, 1000)})function asyncFuncRunner (gen) {
  return function () {
    const iter = gen.apply(this.arguments)
    return new Promise((resolve, reject) = > {
      function step (method, data) {
        let info
        try {
          info = iter[method](data)
        } catch (error) {
          reject(error)
          return
        }
        if (info.done) {
          resolve(info.value)
        } else {
          return Promise.resolve(info.value).then(
            val= > step('next', val),
            err= > step('throw', err)
          )
        }
      }
      step('next')}}}function * generator () {
  const num1 = yield getData()
  console.log(num1)
  const num2 = yield getData()
  console.log(num2)
  const num3 = yield getData()
  console.log(num3)

  return 'OK'
}

const test = asyncFuncRunner(generator)
test().then(
  val= > console.log(val),
  err= > console.log(err)
)

Copy the code

Output result:

[Running] node JavaScript "f: \ \ \ async handwritten API - await. Js." "
161
168
265
OK

[Done] exited with code=0 in 3.129 seconds
Copy the code

If you’re familiar with promises and setTimeout, it’s easy to see that getData() gets a random number between 0 and 519 after 1s.

The code inside the Generator () function should be easy to understand if you understand or are already familiar with generators as I explained earlier.

Let’s focus on asyncFuncRunner(…) This function.

  1. Line 10, return a function, because we’re simulating oneasyncFunction, so return a function
  2. In line 11, the generator is called to generate the iterator, which is used hereapply, mainly for passing values
  3. Line 12, return onepromiseBecause theasyncThe function returns apromise
  4. Line 13 defines onestepFunction to iterate over all asynchronous tasks, arguments in the generatormethodCode executionnext/throwMethod, parameterdataOn behalf ofnext(...)The value to be passed in
  5. Lines 14-20 are to prevent twoyieldThe synchronization code between has an exception thrown, so usetry catchWrapped up,infoisnext()The object returned hasvalueanddoneTwo attributes, if an exception is encounteredreject(err)Then the function completes and returns a failed onepromise
  6. Lines 21 to 23, determine whether all asynchronous tasks are completed, and if so, callresolve(...)Return a successfulpromise, itsvalueIs the return value of the generator function
  7. Lines 23 through 28valueValue converted to successfulpromiseAnd then execute itthenMethod, which continues on success, calling the iteratornext()Method, called on failurethrow()methods
  8. 30 lines, inpromisetheexecutorIn the implementationstep('next')Method to start the generator function

That’s all for this article. Feel free to discuss in the comments section.