又是一阵加班过去,终于空下来充电,文章起因是闲逛看到一篇Medium的文章,介绍作者自己写的库,对React Hooks进行了封装,看了一下挺有意思,就产生了扒源码的冲动
Hooks的暂时痛点
我们知道,在使用Hooks的过程中,尤其是使用主力APIuseEffect替代以前的生命周期时,遇到这样的不习惯,就是无法给useEffect传进去Async函数,官方推荐传进去的函数里面用Promise处理或者内部调用Async函数。具体描述可以参考这一篇文章 How to fetch data with React Hooks? 英文,可机翻
错误的示范👇
会报警告Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}, []);
正确的示范👇
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
即将推出的suspense获取异步数据
不过,这种情况或许会改善一点点,suspense API早在几个月之前就发布了,用来配合lazy作为代码分割、动态引入模块js的使用,大家也不会陌生。不过React官方要正式推出重磅性的suspense异步数据获取功能,到时写法更优雅,逻辑也看起来更清晰,到时候我可能会写关于suspense配合graphql的一篇文章,因为apollo已经进行了实验性质的实现,看起来非常不错。 suspense官方文档地址(英语)
一个封装的可以从外部获取状态方案
无聊的前言说完,下面是正题
Medium原文地址需要翻嫱
useAsyncFunction这个库(npm install use-async-function)使用异步函数,并将其状态耦合到组件的本地状态。用法一看就懂:
import React from 'react';
import useAsyncFunction, { State } from 'use-async-function';
function Users() {
const loadUsers = useAsyncFunction(
() => fetch('/users'),
);
switch (loadUsers.state) {
case State.Fulfilled:
return <UserList>{loadUsers.response}</UserList>;
case State.Rejected:
return <ErrorMessage>{loadUsers.error}</ErrorMessage>;
case State.Pending:
return <LoadingSpinner />;
// 如果尚未调用此函数,请调用它。
default: {
loadUsers();
return <LoadingSpinner />;
}
}
}
看起来我们也能做,把Hooks函数在外面包一层,拿到状态挂到外面变量的静态属性上。
这只是最简单用法,它还支持:
1.传入参数
2.通过传入参数标识的不同,返回对应的各自的函数状态
这个实现起来就有点绕
// 通过用户名异步获取用户数据
function fetchUser({ username }) {
return fetch(`/users/${username}`);
}
// 通过传递参数(用户名)作为唯一标识标记异步函数调用(源码中的reducer)
function idByUsername({ username }) {
return username;
}
function MyComponent() {
const dispatch = useAsyncFunction(fetchUser, idByUsername);
// 我们可以确定是否通过具有该调用ID的属性来进行此调用。
if (!dispatch.admin) {
dispatch({ username: 'admin' });
return null;
}
if (!dispatch.bob) {
dispatch({ username: 'bob' });
return null;
}
// call状态存储在call的ID的属性中。
if (dispatch.admin.state === State.Fulfilled) {
return <div>Admin loaded with {dispatch.admin.value}.</div>;
}
}
这个如果要我们来实现该如何实现呢?
大体思路如下 useAsyncFunction执行肯定要返回一个带有静态属性的可执行函数,这时候 我们先设置一个Map通过id来记录这个方法执行多次中唯一的标识的状态,然后这个函数执行时会把传入的函数执行,并且把promise状态挂载本身的静态属性上,直接上概念代码吧!
function useAsyncFunction(
asyncFunction,
reducer,
){
const Map1 = new Map();
let call2 = (...args)=>{
const id = reducer(...args);
try {
const aValue = await asyncFunction(...args);
Map1.set(id, {
error: undefined,
state: State.Fulfilled,
value: aValue,
});
return aValue;
} catch (e) {
Map1.set(id, {
error: e,
state: State.Rejected,
value: undefined,
});
}
for (const [id, asyncFunctionState] of Map1.entries()) {
(call2)[id] = asyncFunctionState;
}
}
return call2
}
后面我们需要再做一些判断和完善:把call2函数用useMemo包裹一下做个缓存就基本完成了源码的效果,同时还要注意区分不传reducer的情况。
源码(删掉了ts类型定义,方便阅读)
export default function asyncFunctionReducer(
asyncFunctionStateMap,
{ id, ...asyncFunctionState },
){
const newAsyncFunctionStateMap = new Map(asyncFunctionStateMap);
newAsyncFunctionStateMap.set(id, asyncFunctionState);
return newAsyncFunctionStateMap;
}
export default function useAsyncFunction(
asyncFunction,
_reducer = DEFAULT_REDUCER,
_options = DEFAULT_OPTIONS,
){
// Sanitize user input by converting (func, options) to
// (func, reducer, options)
const reducer = typeof _reducer === 'function' ? _reducer : DEFAULT_REDUCER;
const options: Options = typeof _reducer === 'object' ? _reducer : _options;
const [state, setState] = useReducer(asyncFunctionReducer, new Map());
const mounted = useRef(true);
const call1 = useMemo(() => {
const call2= (async (
...args
) => {
const id = reducer(...args);
// Pending
if (mounted.current) {
setState({
error: undefined,
id,
state: State.Pending,
value: undefined,
});
}
try {
const aValue = await asyncFunction(...args);
// Fulfilled
if (mounted.current) {
setState({
error: undefined,
id,
state: State.Fulfilled,
value: aValue,
});
}
return aValue;
} catch (e) {
// Rejected
if (mounted.current) {
setState({
error: e,
id,
state: State.Rejected,
value: undefined,
});
}
// If we are explicitly told to throw an error, do so.
if (options.throwError === true) {
throw e;
}
// If we are not explicitly told to throw an error, return undefined.
return;
}
})
// Assign the state to the async function.这里来区分有没有传reducer进来!
if (reducer === DEFAULT_REDUCER) {
Object.assign(call2, state.get('_'));
} else {
for (const [id, asyncFunctionState] of state.entries()) {
(call2)[id] = asyncFunctionState;
}
}
return call2;
}, [asyncFunction, mounted, options.throwError, reducer, state]);
useEffect(() => () => {
mounted.current = false;
}
, []);
return call1;
}
结语
这就是我喜欢react的原因,学习曲线比较平缓(相比较angular和rx),而且越学越深,值得探索一番。最后我发现前些日子写的一篇不错的文章寥寥阅读,大概是没有推上热点的缘故,这里再厚颜无耻的给自己推荐一下