Writing in the front

Finite-state machines were a required course in my graduate school, which is what most CS graduate students take. This course is simple and easy, but there are so many things and applications in it.

Some people say why is something so simple represented by a seemingly complicated mathematical model? Because a lot of what we know about programming comes from this mathematical model. You may have used this theory for coding without knowing it yourself. Mathematical abstraction allows a more systematic understanding of the theory and its derivation.

Dry saying is tasteless. We can see a finite state machine in some of the things on the front end. So let’s start with some practical applications to understand finite state machines.

This series can be divided into several stinky, long articles, ranging from the simple to the abstract.

Finite state machine

Let’s start with a brief description of finite state machines. The encyclopedia gives a very simple explanation:

Finite-state machine (FSM), also known as finite-state automata, or state machine for short, is a mathematical model that expresses finite states and the transfer and action between these states. – from the wiki

At the heart of the name finite state machine are two words: finite and state. .

A correct finite state machine has only a finite number of states, but the state machine is allowed to run continuously.

The simplest and most commonly used example of a finite state machine is a traffic light, where three colors represent three different states and, depending on certain conditions (in this case, a delay), trigger a transition of states.

The simplest elements that make up a state machine (using traffic lights as an example) :

  1. State set: red, yellow, green;
  2. Initial state: start state of traffic lights;
  3. State transfer function: delay or dynamic scheduling;
  4. Final state: the state in which traffic lights are turned off, which may or may not exist;

Having said that, it’s all mathematical models. So is there a simple finite state machine on the front end? Of course, many, many, many, many, many, many, many, many, many, many, many, many, many.

The realization of the Promise

The relationship between promises and state machines may not be easy to understand, so I’ll leave the conclusion behind and look at a simple Promise implementation code to get a general idea of what finite state machines can do in promises.

Promise is a finite state machine with four states, of which three core states are Pending, depressing and Rejected, indicating that the Promise is suspended, Fulfilled and Rejected, respectively. There is an additional initial state that indicates that the Promise has not yet been implemented. This state is not technically a Promise state, but will be present throughout the actual use of a Promise.

According to the above description, the framework of the finite state machine is roughly built.

So you can implement a Promise on your own from the components of a finite state machine.

Promise

Promise is a nice syntactic candy for asynchronous operations provided by the ES6 standard. It encapsulates the original callback function pattern and implements chain calls for asynchronous operations. It is also easier to use with generator and async syntactic sugar.

Although promises are currently supported in many browsers, many aspects of promises are still not well understood when looking at them. The purpose of writing this code, including its internal implementation mechanism, is to become more familiar with the use of promises.

For details on how to use promises, see this blog post. I won’t explain the use of the Promise object itself, assuming that you already know the basics of how to use promises. If not, see Promise, Generator, and async/await.

See fake-Promise on my Github for the code below.

Initial state:new Promise

First, for ES6 native Promise objects, we pass a function(resolve, reject){} as an argument during initialization, which is used for asynchronous operations.

At present, most asynchronous operations in javascript are carried out in the way of callback. The Promise callback is passed in a function, which takes two parameters, which will be called when the state changes to FULFILLED and REJECTED, respectively.

If an asynchronous operation fails, it is natural to call reject(Err) after processing the cause of the failure.

var p = new Promise(function(resolve, reject) {
  fs.readFile('./readme'.function(err, data) {
    if (err) {
      reject(err);
    } else{ resolve(data); }}); });Copy the code

That is, the two-argument function is called after the asynchronous operation is done anyway.

For this, we can write the Promise constructor (which is the framework and initializer for promise-polyfill) as follows:

const promiseStatusSymbol = Symbol('PromiseStatus');
const promiseValueSymbol = Symbol('PromiseValue');
const STATUS = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
};
const transition = function(status) {
  var self = this;
  return function (value) {
    this[promiseStatusSymbol] = status;
    this[promiseValueSymbol] = value;
  }
}
const FPromise = function(resolver) {
  if(typeof resolver ! = ='function') {
    throw new TypeError('parameter 1 must be a function'); } this[promiseStatusSymbol] = STATUS.PENDING; this[promiseValueSymbol] = []; this.deps = {}; Resolver (// return two functions, resolver and reject. This is a big pity; // This will be a big pity; // This will be a big pity; // This will be a big pity; // This will be a big pity. }Copy the code

After the initialization of the new Promise, the Promise enters its first state, which is the initial state.

The state sets:PENDING,FULFILLED,REJECTED

This is a very disappointing state, which is really Resolved. But the word “Resolved” is too confusing, which will better reflect the meaning of this state.

Based on experience with promises, the entire life cycle should be stateful, with a PENDING state when an asynchronous operation begins but no result, followed by a state of success and failure.

The function passed into the constructor needs to be called inside the constructor to start the asynchronous operation. We then modify the success and failure states and values using the two functions we passed in.

The state machine starts when we call the function wrapped as a Promise. If the asynchronous operation is executed for 10 seconds after startup, the state machine changes from Start to PENDING, indicating that the asynchronous operation is suspended.

The 10-second PENDING state has two branches after the asynchronous operation is complete:

  1. If the asynchronous operation succeeds and no error is thrown, the state machine jumps toFULFILLED;
  2. If the asynchronous operation fails, or an error is thrown, the state machine jumps toREJECTED.

The whole process above is the most fundamental process of the Promise state machine, but promises can be called in a chain, that is, the state machine can make state changes over and over again.

FPromise.prototype.then = function(onFulfilled, onRejected) {
  const self = this;
  return FPromise(function(resolve, reject) {
    const callback = function() {// The return value of the callback function is also saved, because when the chain call is made, This parameter should be passed to the next chained call // resolve function const resolveValue = onFulfilled(self[promiseValueSymbol]); resolve(resolveValue); } const errCallback =function() { const rejectValue = onRejected(self[promiseValueSymbol]); reject(rejectValue); } // Here is the processing of the current Promise state, if the previous Promise is executingthenThe method has already been // completed, so the next Promise callback should be executed directlyif (self[promiseStatusSymbol] === STATUS.FULFILLED) {
      return callback();
    } else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
      return errCallback();
    } else if(self[promiseStatusSymbol] === STATUS.PENDING) { self.deps.resolver = callback; self.deps.rejecter = errCallback; }})}Copy the code

The then method should be at the root of the chain calls that Promise makes.

First, the THEN method takes two parameters, a successful and a failed callback,

It should then return a new Promise object to call the next node in the chain,

Finally, if the Promise object status is FULFILLED or REJECTED, then you can call the callback function directly. Otherwise, you need to wait for the completion state of the asynchronous operation to happen.

State transition function: chain call

Strictly speaking, each state transition is judged based on the execution state of the current asynchronous operation. But each iteration of the asynchronous operation is a chain operation that relies on the Promise, otherwise the state machine would not have generated so many state transitions.

Chained calls are based on dependency collection, and in general, the code in promises is asynchronous, making it impossible to execute the function within the callback immediately when the function is executed.

if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
	return callback();
} else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
	return errCallback();
} else if(self[promiseStatusSymbol] === status.pending) {self[promiseStatusSymbol] === status.pending; self.deps.rejecter = errCallback; }Copy the code

After the dependencies are gathered together, when the state changes, we use a setter to respond to the state change and perform the corresponding callback.

const transition = function(status) {
  return (value) => {
    this[promiseValueSymbol] = value;
    setStatus.call(this, status); }} /** * Controls state changes, similar to the effects of accessors. This is a big pity function, which is FULFILLED if the state is from PENDING --> FULFILLED, then call the next onFulfilled function of the chain. Call the next onRejected function * * @returns void */ constsetStatus = function(status) {
  this[promiseStatusSymbol] = status;
  if (status === STATUS.FULFILLED) {
    this.deps.resolver && this.deps.resolver();
  } else if(status === STATUS.REJECTED) { this.deps.rejecter && this.deps.rejecter(); }}Copy the code

When the first asynchronous execution is complete, resolver or rejecter of its dependencies will be executed. Then we return a new Promise in the resolver, which is then used to execute p1. P2 has the same structure as P1, and after it is constructed, it also performs dependency collection and chain calls, forming a finite state machine with multiple state cycles.

Finite state machine andPromise

At this point, you should see the connection between finite state machines and promises. In fact, a Promise is a finite state machine like a traffic light, in addition to relying on the collection process.

Promise basically has all the major elements of a finite state machine. A Promise state machine controls the synchronous execution of asynchronous functions through state transitions during its life cycle, partly guaranteeing callback hell for callback functions.

In addition to the simpler Promise implementation, which uses a finite state machine mathematical model, there are other state machine-related practices on the front end. And there are very complex practices. The next article will cover the implementation of Redux and its relation to automata (although I don’t know if there will be time for this business iteration cycle…).