阅读 257

ReactHooks封装库useAsyncFunction源码分析

又是一阵加班过去,终于空下来充电,文章起因是闲逛看到一篇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类型定义,方便阅读)

github地址

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),而且越学越深,值得探索一番。最后我发现前些日子写的一篇不错的文章寥寥阅读,大概是没有推上热点的缘故,这里再厚颜无耻的给自己推荐一下

实现一个简单的React16新版context

关注下面的标签,发现更多相似文章
评论