上一篇完成了页面的搭建,这一篇将完成状态机的代码编写。
项目中用到了两个状态机:
covidMachine
:父状态机,存储所有国家信息(countryList
)和选中国家的新冠数据(countryData
)covidDataMachine
:子状态机,存储指定国家的新冠数据(countryData
)
父状态机只有一个,子状态机可以多个(取决于用户选了多少个国家) 。
状态机分析
子状态机是当我们通过下拉框选择某个国家时,才进行创建的;选了多少个国家,就会有多少个子状态机;某个国家的新冠数据保存在对应的子状态机中,父状态机只要维护好子状态机的引用就行。
也就是说,父状态机的 context
里面会有静态数据(countryList
)和动态数据,静态数据就是一次获取后不再更改的所有国家信息;动态数据则是一个动态引用,引用了某个国家的新冠数据。
相关概念
在编写状态机之前,先了解一下 invoke
和 spawn
。
Invoke Service
调用服务
invoke
是状态节点的配置属性,主要用来调用其他的服务,这些服务可以包括:promise
、Callbacks
、Observable
、Machine
。
调用这些服务,是为了获取特定的资料。比如在这个项目中,我们需要通过访问特定的网站,去获取所有的国家的资料,就可以通过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
属性,这个属性用来监听子组件数据加载完成。因为状态机之间的数据不是共享的,父状态机无法知道子状态机发生的事件,他们之间需要通过发送事件和监听事件来完成通信
父状态机可视化图表:
子状态机
子状态机也就是上面提到的参与者,他指代某个特定的国家,他的 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")
值,这是子状态机向父状态机发送一个事件,便是数据获取完成
子状态机可视化图表:
下一章我们会综合运用。点击跳转:
线上Demo
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。