1 overview

This issue of intensive reading is the finite state machine management tool Robot source.

Finite state machine refers to a mathematical model that switches between a finite number of states. Finite state is very common in business and game development, including sending requests, which is a finite state machine model.

I will introduce how the library is used in the introduction, explain the implementation principles in the close reading, and summarize the value of using the library in business.

2 brief introduction

The core of this library is to create a finite state machine using createMachine:

import { createMachine, state, transition } from 'robot3';

const machine = createMachine({
  inactive: state(
    transition('toggle'.'active')
  ),
  active: state(
    transition('toggle'.'inactive'))});export default machine;
Copy the code

As shown in the figure above, we created a finite state machine that has two states: Inactive and Active, and can switch between the two states by toggle.

React with React, React -robot:

import { useMachine } from 'react-robot';
import React from 'react';
import machine from './machine'
 
function App() {
  const [current, send] = useMachine(machine);
  
  return (
    <button type="button" onClick={() => send('toggle')}>
      State: {current.name}
    </button>
  )
}
Copy the code

The current. Name obtained from the useMachine indicates the current status value, and send is used to send the command to change the status.

As for why we use finite state machine management tools, the official document gives an example – click edit to enter the edit state, click Save to return to the original state:

After clicking Edit button, you will enter the state as shown below. After clicking Save, if the input content is verified and saved, you will return to the initial state:

If we don’t use a finite state machine, we first create two variables to store whether we are in edit state and what the current input text is:

let editMode = false;
let title = ' ';
Copy the code

If you consider the interaction with the back end, you will add three states – save, verify, save successfully:

let editMode = false;
let title = ' ';
let saving = false;
let validating = false;
let saveHadError = false;
Copy the code

Even with frameworks like React and Vue that drive UI data, complex state management is inevitable. If implemented using a finite state machine, it would look like this:

import { createMachine, guard, immediate, invoke, state, transition, reduce } from 'robot3';

const machine = createMachine({
  preview: state(
    transition('edit'.'editMode'.// Save the current title as oldTitle so we can reset later.
      reduce(ctx= > ({ ...ctx, oldTitle: ctx.title }))
    )
  ),
  editMode: state(
    transition('input'.'editMode',
      reduce((ctx, ev) = > ({ ...ctx, title: ev.target.value }))
    ),
    transition('cancel'.'cancel'),
    transition('save'.'validate')),cancel: state(
    immediate('preview'.// Reset the title back to oldTitle
      reduce(ctx= > ({ ...ctx, title: ctx.oldTitle })
    )
  ),
  validate: state(
    // Check if the title is valid. If so go
    // to the save state, otherwise go back to editMode
    immediate('save', guard(titleIsValid)),
    immediate('editMode')
  )
  save: invoke(saveTitle,
    transition('done'.'preview'),
    transition('error'.'error')),error: state(
    // Should we provide a retry or... ?)});Copy the code

Immediate indicates that the state jumps to the next state, and reduce can expand the internal data of the state machine. For example, if preview returns oldTitle, then cancle returns ctx.oldTitle; Invoke indicates that state is invoked after the first function is called.

We can see the benefits of using a state machine in the code above:

  1. State is clear. List all states of a business logic first to avoid omission.
  2. State transition security. Such aspreviewI can only switch toeditState, so that even in the wrong state to send the wrong instruction will not generate an exception.

3 intensive reading

Robot’s important functions include createMachine, state, transition, and immediate.

createMachine

CreateMachine creates a state machine:

export function createMachine(current, states, contextFn = empty) {
  if(typeofcurrent ! = ='string') {
    contextFn = states || empty;
    states = current;
    current = Object.keys(states)[0];
  }
  if(d._create) d._create(current, states);
  return create(machine, {
    context: valueEnumerable(contextFn),
    current: valueEnumerable(current),
    states: valueEnumerable(states)
  });
}
Copy the code

Keys (States)[0] to get the first state as the current state (marked in current). This will eventually save three properties:

  • contextThe current state machine internal property, initialization is empty.
  • currentCurrent status.
  • statesAll of the states, which is equal tocreateMachineThe first argument passed.

Now look at the create function:

let create = (a, b) = > Object.freeze(Object.create(a, b));
Copy the code

That is, an unmodified object is created as the state machine.

Here is the machine object:

let machine = {
  get state() {
    return {
      name: this.current,
      value: this.states[this.current] }; }};Copy the code

That is, the internal state management of the state machine is done through objects, and the state() function is provided to get the current state name and state value.

state

State describes which transitions are supported by the state:

export function state(. args) {
  let transitions = filter(transitionType, args);
  let immediates = filter(immediateType, args);
  let desc = {
    final: valueEnumerable(args.length === 0),
    transitions: valueEnumerable(transitionsToMap(transitions))
  };
  if(immediates.length) {
    desc.immediates = valueEnumerable(immediates);
    desc.enter = valueEnumerable(enterImmediate);
  }
  return create(stateType, desc);
}
Copy the code

Transitions and immediates represent the transitions or immediate results taken from ARgs.

Transition and immediate are defined as follows:

export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);

function filter(Type, arr) {
  return arr.filter(value= > Type.isPrototypeOf(value));
}
Copy the code

So if a function is passedimmediateCreated, can passimmediateType.isPrototypeOf()This method is applicable to a wide range of, in any library can be used to check to get the corresponding function created objects.

If the number of arguments is 0, the state is final and cannot be converted. Finally, create creates an object that is the value of the state.

transition

Transition is a function written in state that describes how the current state can be changed. Its actual function is makeTransistion:

function makeTransition(from, to, ... args) {
  let guards = stack(filter(guardType, args).map(t= > t.fn), truthy, callBoth);
  let reducers = stack(filter(reduceType, args).map(t= > t.fn), identity, callForward);
  return create(this, {
    from: valueEnumerable(from),
    to: valueEnumerable(to),
    guards: valueEnumerable(guards),
    reducers: valueEnumerable(reducers)
  });
}
Copy the code

Due to:

export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);
Copy the code

Visible from null indicates an immediate transition to the state to. Transition finally returns an object in which Guards was found from the Transition or immediate argument, an object created by the Guards function. This state takes effect only when the object’s callback is successfully executed.

. Args corresponds to transition(‘toggle’, ‘active’) or immediate(‘save’, guard(titleIsValid)), Stack (filter(guardType, args).map(t => t.fn), truthy, callBoth) Find whether there are Guards and reducers in ARGS.

Finally, to see how the state changes, the function that sets the state change is transitionTo:

function transitionTo(service, fromEvent, candidates) {
  let { machine, context } = service;
  for(let { to, guards, reducers } of candidates) {  
    if(guards(context)) {
      service.context = reducers.call(service, context, fromEvent);

      let original = machine.original || machine;
      let newMachine = create(original, {
        current: valueEnumerable(to),
        original: { value: original }
      });

      let state = newMachine.state.value;
      returnstate.enter(newMachine, service, fromEvent); }}}Copy the code

As you can see, if Guards exist, the state needs to change properly until the Guards execution returns successfully. Reducers can also modify context in service.context = reducers. Call (service, context, fromEvent); This row shows it. Finally, a new state machine is generated and current is marked to.

Finally, let’s look at the state.enter function, which is defined in the state function and essentially inherits stateType:

let stateType = { enter: identity };
Copy the code

The identity function executes immediately:

let identity = a= > a;
Copy the code

So you’re essentially returning a new state machine.

4 summarizes

Compared with common business description, finite state machine actually adds some constraints on state transformation to optimize state management. Moreover, state description is more standardized and has certain practicability in business.

Of course, not all businesses will be able to use finite state machines, as the new framework still has some learning costs to consider. Finally, through the source code learning, we have learned some new frame-level tips, can be flexibly applied to their own framework.

The discussion address is: intensive reading robot source-Finite state machines · Issue #209 · dT-fe /weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.

Pay attention to the front end of intensive reading wechat public account

Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)