展望 react-cache,一个 React 官方的处理数据副作用方案

时间:2021-1-8 作者:admin

我自认不比写 React 的人更懂 React ,因此相关的哲学理念层面上的思考、相比传统方式的优劣的讨论我就不献丑了,请大家自行阅读这篇 官方文档 ,这篇文章只介绍 react-cache 的使用方式和原理。

Suspense

相信做过 React 代码分割的同学基本上对 Suspense 都比较了解,但是 Suspense 其实并不是局限于加载异步组件,而是有着一种更通用的范围。为了更好的理解 react-cache 的原理,我们事先需要了解 Suspense 的运作流程。

错误边界(Error Boundaries)

Suspense 的底层实现依赖于 错误边界(Error Boundaries) 组件,从描述中我们知道, 错误边界 是一种组件,生成一个 错误边界 组件也很容易,任何实现了 static getDerivedStateFromError() 静态方法的 class 组件 就是一个 错误边界 组件。

错误边界 组件的主要作用在于, **错误边界 组件能够捕获子组件(不包括自身) throw 出的 Error** ,如以下示例

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

错误边界 使我们在子组件树崩溃时,可以渲染 备用UI 而非 错误UI,那么这又和 Suspense 有什么关系呢?

刚才我们说了 错误边界 组件能够捕获子组件(不包括自身) throw 出的 Error ****,这句话其实并不是完全正确,应该是 错误边界 组件能够捕获子组件(不包括自身) throw 出的 任何东西 。可以将 Suspense 当做一种特殊 错误边界 组件,当 Suspense 捕获到子组件抛出的时 Promise 时会暂时挂起 Promise 渲染 fallback UI ,当其 Resolved 之后重新渲染。

react-cache

react-cache 暂时处于实验性阶段,是对 React 如何获取数据的一种新的思考方式

先来快速看下使用方式

// app.jsx

import { getTodos, getTodoDetail } from './api';
import { unstable_createResource as createResource } from 'react-cache';
import { Suspense, useState } from 'react';

const remoteTodos = createResource(() => getTodos());
const remoteTodoDetail = createResource((id) => getTodoDetail(id));

const Todo = (props) => {
  const [showDetail, setShowDetail] = useState(false);

  if (!showDetail) {
    return (
      <li onClick={() => {
        setShowDetail(true);
      }}>
          <strong>{props.todo.title}</strong>
      </li>
    );
  }

  const todoDetail = remoteTodoDetail.read(props.todo.id);

  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};

function App() {
  const todos = remoteTodos.read();

  return (
    <div className="App">
      <ul>
        {todos.map(todo => (
          <Suspense key={todo.id} fallback={<div>loading detail...</div>}>
            <Todo todo={todo} />
          </Suspense>
        ))}
      </ul>
    </div>
  );
}

export default App;

// index.jsx

ReactDOM.render(
  <React.StrictMode>
    <Suspense fallback={<div>fetching data...</div>}>
      <App />
    </Suspense>
  </React.StrictMode>,
  document.getElementById('root')
);

效果演示

API

  • unstable_createResource

react-cache 有两个 API ,但是核心函数就一个 unstable_createResource ,我们使用他来创建适用于 Suspense 的数据拉取函数 remoteTodosremoteTodoDetail

unstable_createResource 接受两个参数,第一个必选,第二个可选。第一个参数是是一个函数,其返回值要求必须是 Promise ,第二个参数是可选的,接受一个 哈希 函数,主要作用是为了区别在复杂输入情况下对应数据缓存的情况,这个部分等下会再讲,这里先带过。

  • unstable_setGlobalCacheLimit

用于设置全局的 react-cache 的缓存数量限制

原理

由于 react-cache 的代码很少,我们直接看下源码实现

export function unstable_createResource<I, K: string | number, V>(
  fetch: I => Thenable<V>,
  maybeHashInput?: I => K,
): Resource<I, V> {
  const hashInput: I => K =
    maybeHashInput !== undefined ? maybeHashInput : id => id;
        // 简单输入的情况,默认哈希函数就够用了
  const resource = {
    read(input: I): V {
      const key = hashInput(input);
      // 生成特定输入对应的 key
      const result: Result<V> = accessResult(resource, fetch, input, key);
      switch (result.status) {
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },

    preload(input: I): void {
      // react-cache currently doesn't rely on context, but it may in the
      // future, so we read anyway to prevent access outside of render.
      readContext(CacheContext);
      const key = hashInput(input);
      accessResult(resource, fetch, input, key);
    },
  };
  return resource;
}

当你执行 unstable_createResource 后会返回一个带有 .read.preload 方法的对象, .preload 很简单,我们着重讲下 .read

  • 当在 React 组件里调用 .read()
  • 会通过 maybeHashInput() 生成的 key 去查缓存
  • 如果有 同步返回
  • 没有则执行创建对象时传入的数据拉取函数,生成一个 Promise 同时 throw
  • 最近的祖先 Suspense 组件捕获到这个 Promise ,挂起,渲染 fallback UI
  • Promise resolved 后, react-cache 内部监听了 Promise ,会将 resolved 的值设置到缓存中
  • 同时 Suspense 组件也发现 Promise resolved ,重新渲染子组件
  • 子组件再次执行 .read() 方法,通过 key 去查缓存,发现已经缓存过,同步返回,然后进行渲染,自此整个流程结束

其他

react-cache 内部的缓存机制使用 LRU 策略,这里就不多讲了,整个使用下来最大感受其实是,我们在以一种同步的思维来写,即我们认为数据是已经存在的,我们只是做的读取操作,而非 拉取

const Todo = (props) => {
  const [showDetail, setShowDetail] = useState(false);

  if (!showDetail) {
    return (
      <li onClick={() => {
        setShowDetail(true);
      }}>
          <strong>{props.todo.title}</strong>
      </li>
    );
  }

  const todoDetail = remoteTodoDetail.read(props.todo.id);

  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};

不需要考虑如何使用 useEffect ,而是以一种自然而然方式书写组件:

  • 读取数据
  • 渲染 UI

而非

  • 渲染组件
  • 考虑 loading 态
  • 触发生命周期
  • 进行数据拉取
  • 考虑拉取时组件状态
  • 设置到 state 上
  • 重新渲染 UI

让我们看看上述代码,如果使用传统的书写方式会是什么样

const Todo = (props) => {
  const [showDetail, setShowDetail] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [todoDetail, setTodoDetail] = useState(null);

  useEffect(() => {
    if (showDetail) {
      setIsLoading(true);
      getTodoDetail(props.todo.id)
        .then(todoDetail => setTodoDetail(todoDetail))
        .finally(() => {
          setIsLoading(false);
        })

    }
  }, [showDetail, props.todo.id]);

  if (isLoading) {
    return <div>loading detail...</div>;
  }

  if (!showDetail) {
    return (
      <li onClick={() => {
        setShowDetail(true);
      }}>
          <strong>{props.todo.title}</strong>
      </li>
    );
  }

  if (todoDetail === null) return null;
  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};

对于想实际上手把玩下的,示例代码已推送到 github仓库

WARN

如果你尝试下载试用 react-cache ,那么大概率你会遇到 TypeError: Cannot read property 'readContext' of undefined 问题,从 issue 上看似乎是因为代码使用了未发布的私有的 context 相关的 API ,不过由于 context 相关的代码仅仅是处于 TODO 阶段,并没有实际的作用, 因此有两种解决方法

详见这两个 issue

解决办法

  • 自己将 react-cachenode_modules 里面复制出来,然后手动将 readContext 相关的代码注释掉
  • 使用从 github 仓库源码直接构建的 react-cache ,可以将以下代码写入到 package.json ,然后执行即可
    • 构建过程会用到 java,记得安装
"postinstall": "git clone https://github.com/facebook/react.git --depth=1 && cd react && yarn install --frozen-lockfile && npm run build react-cache && cd .. && npm i $(npm pack ./react/build/node_modules/react-cache) && rm -rf react react-cache-*.tgz"
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。