3

上一篇完成了页面的搭建,这一篇将完成状态机的代码编写。

项目中用到了两个状态机:

  • covidMachine :父状态机,存储所有国家信息(countryList)选中国家的新冠数据(countryData)
  • covidDataMachine :子状态机,存储指定国家的新冠数据(countryData)

父状态机只有一个,子状态机可以多个(取决于用户选了多少个国家) 。

状态机分析

子状态机是当我们通过下拉框选择某个国家时,才进行创建的;选了多少个国家,就会有多少个子状态机;某个国家的新冠数据保存在对应的子状态机中,父状态机只要维护好子状态机的引用就行。

也就是说,父状态机的 context 里面会有静态数据(countryList)动态数据,静态数据就是一次获取后不再更改的所有国家信息;动态数据则是一个动态引用,引用了某个国家的新冠数据。

image

相关概念

在编写状态机之前,先了解一下 invokespawn

Invoke Service 调用服务

invoke 是状态节点的配置属性,主要用来调用其他的服务,这些服务可以包括:promiseCallbacksObservableMachine

调用这些服务,是为了获取特定的资料。比如在这个项目中,我们需要通过访问特定的网站,去获取所有的国家的资料,就可以通过invoke promise去调用我们的请求。

invoke 过程中,还提供了一些生命钩子:

// invoke配置
loading: {
  invoke: {
    src: someSrc, // 服务的来源
    onDone: {/* ... */}, // 调用完成后执行
      onError:{/* ... */}  // 调用失败后执行
    // ...
  },
}

Spawning Actors 创建参与者

Actor 模型中,每个状态机实例都被认为是一个“参与者” ,可以向其他“参与者”发送和接收事件(消息) ,并对它们做出反应。

spawn 可以动态添加“参与者”,然后返回这个参与者的引用:

// 创建参与者,获取引用
countryRef = spawn(createCovidDataMachine(event.name));

在本项目中,我们在下拉框选择某个国家后,进行 Spawning Actors ,然后把这个引用返回给父状态机。

代码实现

父状态机

这里把fetchCountriesState单独抽离了出来,是因为这些状态都只有一个目的:invoke promise 获取所有国家的信息。

// covidMachine.js
import { Machine, assign, spawn } from "xstate";
import { createCovidDataMachine } from "./covidDataMachine";

const URL_COUNTRIES = "https://covid19.mathdro.id/api/countries";

// 立即执行,获取国家列表
const fetchCountriesState = {
  initial: "loading",
  states: {
    loading: {
      invoke: {
        id: "fetch-countries",
        src: "getListCountries",
        onDone: {
          target: "loaded",
          actions: assign({
            listCountries: (_, event) => event.data.countries
          })
        },
        onError: {
          target: "failure"
        }
      }
    },
    loaded: {
      type: "final"
    },
    failure: {
      on: {
        RETRY: "loading"
      }
    }
  }
};

export const covidMachine = Machine(
  {
    id: "covid-machine",
    initial: "idle",
    context: {
      // 国家列表
      listCountries: [],
      // 当前国家的引用
      countryRef: null,
      // 缓存
      countryCashMap: {}
    },
    states: {
      idle: {
        ...fetchCountriesState
      },
      selected: {},
      completed: {}
    },
    on: {
      SELECT: {
        target: ".selected",
        actions: "initContext"
      },
      // 接受子状态机发送的事件
      "EVENT.LOADED": "completed"
    }
  },
  {
    actions: {
      // 初始化上下文
      initContext: assign((context, event) => {
        // 获取引用
        let countryRef = context.countryCashMap[event.name];

        if (countryRef) {
          return {
            ...context,
            countryRef
          };
        }
        // 添加参与者,保存引用
        countryRef = spawn(createCovidDataMachine(event.name));
        return {
          ...context,
          countryRef,
          countryCashMap: {
            ...context.countryCashMap,
            [event.name]: countryRef
          }
        };
      })
    },

    services: {
      getListCountries: () => fetch(URL_COUNTRIES).then(res => res.json())
    }
  }
);

有几个需要关注的点:

  • fetchCountriesState目的单一,抽出来便于管理
  • 父状态机的 context 中的 countryCashMap 对象,是为了缓存查询过的国家(参与者),如果下次再选到之前查询过的国家,就不再发送请求,直接从缓存中查找到该国家(参与者)
  • 父状态机监听的事件中,有个EVENT.LOADED属性,这个属性用来监听子组件数据加载完成。因为状态机之间的数据不是共享的,父状态机无法知道子状态机发生的事件,他们之间需要通过发送事件和监听事件来完成通信

父状态机可视化图表:
image

子状态机

子状态机也就是上面提到的参与者,他指代某个特定的国家,他的 context 里面存有我们通过 invoke promise 获取的该国家的新冠动态

// covidDataMachine.js
import { Machine, assign, sendParent } from "xstate";

const COVID_DATA_URL = "https://covid19.mathdro.id/api";

// 用工厂方法创建
export const createCovidDataMachine = country =>
  Machine(
    {
      id: "covid-data",
      initial: "loading",
      context: {
        country,
        // 确诊
        confirmed: null,
        // 康复
        recovered: null,
        // 死亡
        deaths: null,
        // 上一次更新时间
        lastUpdateTime: null
      },
      states: {
        loading: {
          invoke: {
            id: "fetch-covid-data",
            src: "getStatistics",
            onDone: {
              target: "loaded",
              actions: "handleCovidData"
            }
          }
        },
        loaded: {
          type: "final",
          entry: sendParent("EVENT.LOADED")
        },
        failure: {
          on: {
            RETRY: "loading"
          }
        }
      }
    },
    {
      actions: {
        handleCovidData: assign({
          // 确诊
          confirmed: (_, event) => event.data.confirmed.value,
          // 康复
          recovered: (_, event) => event.data.recovered.value,
          // 死亡
          deaths: (_, event) => event.data.deaths.value,
          // 上一次更新时间
          lastUpdateTime: (_, event) => event.data.lastUpdate
        })
      },
      services: {
        getStatistics: context => {
          let changedUrl = COVID_DATA_URL;

          if (context.country !== undefined) {
            changedUrl = `${COVID_DATA_URL}/countries/${context.country}`;
          }
          return fetch(changedUrl).then(response => response.json());
        }
      }
    }
  );

有几个需要关注的点:

  • 子状态机不是马上产生,这里返回了一个工厂函数,因为我们是通过交互去添加子状态机(参与者)的
  • 状态节点中有个sendParent("EVENT.LOADED")值,这是子状态机向父状态机发送一个事件,便是数据获取完成

子状态机可视化图表:
image
下一章我们会综合运用。点击跳转:

线上Demo

中原大虾
123 声望3 粉丝