❓ 什么是 XState ?
🆚 状态机 VS HOOKS 示例
- 需求描述:根据输入的关键字进行搜索,并将搜索结果显示出来,并允许用户取消请求。
- hooks 实现流程
- 定义 state
- dataList: 存放请求结果
- loading:接口响应比较慢时的loading效果;当用户在等待搜素请求的时候,结果返回前,禁止再次发送请求:
- isError:接口出错状态
- fetchAbort: 取消请求的 AbortController
- 定义请求方法
- 请求未完成之前,禁止用户再次发起请求
- 请求前
标记 loading
- 请求成功 之后
保存结果
,清除error状态
,清除loading状态
- 请求失败
标记error状态
,清除loading状态
- 定义取消请求方法
- 调用前需要判断
loading状态
,fetchAbort是否存在
- 调用前需要判断
function useSearch() {
const SEARCH_URL = "/edrawsoft/mindmap/1/PV/DESC/CN/1/";
// 请求结果
const [dataList, setDataList] = useState([]);
// loading 作用:1,UI loading 样式 2,当用户在等待搜素请求的时候,结果返回前,禁止再次发送请求:
const [loading, setLoading] = useState(false);
// 接口出错了
const [isError, setIsError] = useState(false);
// 用来中止一个请求
const fetchAbort = useRef(null);
// 请求数据
const fetchData = useCallback((keyword) => {
if (!loading) { // 当用户在等待搜素请求的时候,结果返回前,禁止再次发送请求:
setLoading(true);// 请求开始,loading 设置为true
fetchAbort.current = new AbortController();
fetch(SEARCH_URL + keyword, { signal: fetchAbort.current.signal })
.then(response => response.text())
.then(str => new window.DOMParser().parseFromString(str, "text/xml"))
.then((data) => {
setIsError(false);// 请求成功了,清除错误状态
setDataList(Array.from(data.documentElement.children[0].children)
.filter(item => item.tagName === 'item'))
})
.catch((e) => {
if (e && e.name === "AbortError") { // 手动中止请求
// 手动取消请求,啥也不干
} else {
setIsError(true);// 标记错误状态
}
})
.finally(() => {
setLoading(false);// 记得把 Loading 关掉:
});
}
}, [loading]);
// 取消请求
const cancelFetch = useCallback(() => {
if (loading && fetchAbort.current) { // 请求时才能取消请求
fetchAbort.current.abort();
}
}, [loading]);
return { loading, dataList, isError, fetchData, cancelFetch };
}
页面使用
function AppHooks() {
const { loading, dataList, isError, fetchData, cancelFetch } = useSearch();
return (
<div className="app-hook">
<h3>app-hook</h3>
<Search onSearch={fetchData} disabled={loading} />
<button onClick={cancelFetch}>取消请求</button>
<p>{loading ? "loading...." : " "}</p>
<p>{isError ? "Error...." : " "}</p>
<ul className="app-res-list">
{dataList.map(item => <li key={item.children[4].innerHTML}>
<a href={item.children[4].innerHTML}>{item.children[0].innerHTML}</a>
</li>)}
</ul>
</div>
);
}
平常使用的都是这种事项方式,也对他存在的问题感同身受:
- 难以阅读理解,包含各种分散使用的
flag
变量和嵌套着各种if/else
的代码,由于功能的快速迭代,来不及留下业务流程或者代码逻辑说明,所有逻辑流程只存在开发当时的脑子里,后边其他同学或者自己开发时得从头梳理一遍逻辑,才能继续开发,如果没有注释的话可能就💣了。 - 难以扩展,如果是很久远的代码,不好理解,不清楚为什么这么写,写了有啥作用,导致老的逻辑不敢动,新的逻辑不敢加。
- 可能含有隐藏的 Bug;边界不清楚,容易遗漏一些条件判断。
- 新功能增加时使逻辑进一步混乱,老的逻辑不能动,就只能在老逻辑上新增判断逻辑,结果代码逻辑就越来越长。
- 看下用 XState 怎么实现
- 定义一个状态机
fetchMachine
- 定义
ready
,loading
,error
,success
,userCancel
5个状态。 - 定义初始状态
ready
。 - 定义一个上下文,也就是存请求结果的地方:
dataList
。 - 定义一个
fetch-data
子状体机 ,他和fetchMachine
的关系可以理解为 父子组件的关系,他接受两个事件FETCH
用来发起请求,CANCEL_FETCH
用来取消请求。请求成功后 触发fetchMachine
的FETCH_SUCCESS
事件,失败时 触发fetchMachine
的FETCH_ERROR
事件 ready
,error
,success
,userCancel
这几个状态 直接收FETCH_DATA
事件,就是只能发起请求,触发时转为loading
状态。loading
进入这个状态时,会触发fetch-data
的FETCH
事件,用来发起请求。可以接受以下几个事件。FETCH_SUCCESS
请求成功,触发时更新dataList
,状态机转换为success
状态;FETCH_ERROR
请求失败,状态机转换为error
状态;FETCH_CANCEL
用户取消请求,状态机转换为userCancel
状态;
- 定义
// 请求 fetch-data 子状体机
function invokeFetchData() {
let fetchAbort = null; // 取消请求 AbortController
const SEARCH_URL = "/edrawsoft/mindmap/1/PV/DESC/CN/1/";
// 请求接口
function fetchData(keyword) {
fetchAbort = new AbortController();
return fetch(SEARCH_URL + keyword, { signal: fetchAbort.signal })
.then(response => response.text())
.then(str => new window.DOMParser().parseFromString(str, "text/xml"))
}
// 取消请求
function cancelFetch() {
fetchAbort.abort();
}
return function invokeFetchDataFn(sendBack, onReceive) {
onReceive(e => {
if (e.type === "FETCH") {
fetchData(e.data)
.then(data => {
sendBack({
type: "FETCH_SUCCESS",
data: Array.from(data.documentElement.children[0].children)
.filter(item => item.tagName === 'item')
})
})
.catch(err => {
if (err.name !== "AbortError") {
sendBack({ type: "FETCH_ERROR", err });
}
})
} else if (e.type === "CANCEL_FETCH") {
cancelFetch();
}
})
}
}
const fetchMachine = createMachine(
{
id: 'fetchMachine',
initial: 'ready',
context: {
dataList: [], // 请求结果
},
invoke: [{
id: "fetch-data",
src: "invokeFetchData"
}],
states: {
ready: { // 空闲
on: {
"FETCH_DATA": { target: 'loading' }
}
},
loading: { // 请求中
entry: [send((_, e) => ({ type: "FETCH", data: e.data }), { to: "fetch-data" })], // 告诉 fetch-data 调用接口
on: {
"FETCH_SUCCESS": { // 接口调用成功了
target: "success",
actions: assign({ dataList: (_, e) => e.data }), // 更新dataList
},
"FETCH_ERROR": { // 接口调用失败了
target: "error"
},
"FETCH_CANCEL": { // 用户取消了请求
target: "userCancel",
actions: send({ type: "CANCEL_FETCH" }, { to: "fetch-data" }), // 告诉 fetch-data 取消请求
}
}
},
error: { // 请求成功
on: {
"FETCH_DATA": { target: 'loading' }
}
},
success: { // 请求失败
on: {
"FETCH_DATA": { target: 'loading' }
}
},
userCancel: { // 用户取消了请求
on: {
"FETCH_DATA": { target: 'loading' }
}
}
}
},
{
services: {
invokeFetchData
}
}
);
如果VSCode 装了 XState VSCode 插件的话可以自动生成状体图
页面使用:
function AppMatchine() {
const [state, send] = useMachine(fetchMachine);
return (
<div className="app-machine">
<h3>app-machine</h3>
<Search onSearch={(keyword) => send({ type: "FETCH_DATA", data: keyword })} disabled={state.matches("loading")} />
<button onClick={() => send("FETCH_CANCEL")}>取消请求</button>
<p>{state.matches("loading") ? "loading...." : " "}</p>
<p>{state.matches("error") ? "Error...." : " "}</p>
<ul className="app-res-list">
{state.context.dataList.map(item => <li key={item.children[4].innerHTML}>
<a href={item.children[4].innerHTML}>{item.children[0].innerHTML}</a>
</li>)}
</ul>
</div>
);
}
可以显著的看出以下几个优点:
- 逻辑清晰,什么状态能干什么事以及接受到事件后会转移到什么状态 可以很容易看出来。
- 状态图,可以自动生成状态图,方便查看整体逻辑。
- 页面使用简单,没有很多变量的判断。
🤔 思考
- 使用 XState 有哪些好处?
- UI 组件逻辑 和 业务逻辑 拆分,比如按钮组件只需要知道它是可点击的还是禁止点击的,以及点击它会触发一个事件。至于他为什么可以点击,触发的事件 搜索 还是 重置 并不需要关心。
- 状态图可以帮助我们关注应用所有可能的状态、状态之间的相互关系以及每个状态中可能发生的操作,从而消除所有可能存在的问题/错误场景,无论是代码问题还是 UI 问题。它也会迫使我们思考什么的状态机可以适用到视图或 UI 组件上。
- 协作的问题,有了图,就好沟通了。
- 有了图,便于记忆整个逻辑关系,例如 TCP 有限状态机
- 只有优点吗?
- 新的功能,新的页面感觉可能会用一下,老的逻辑如果要使用的话,时间成本和风险有点儿大。
- 学习和试错成本问题,相对简单的逻辑还可以,相对复杂的逻辑不知道能不能解决。