gnosis23 / hello-world-blog

还是 issues 里面写文章方便
https://bohao.work
0 stars 0 forks source link

swr是个好东西 #85

Open gnosis23 opened 3 years ago

gnosis23 commented 3 years ago

在使用了一段时间的 swr 后,它已经加入了“我的最爱”工具库了😂 s w r

下面结合我的个人经验,谈谈为什么你应该使用它。

接口请求状态

在请求接口的时候,一般都需要维护请求的状态。比如加载列表的时候,通常都需要区分下 loading、empty、success、error 等状态,然后更新相关的视图,看上去就是下面的代码

const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// more states...

const sendRequest = useCallback(() => {
  setLoading(true);
  fetchData()
    .then(data => {
      setLoading(false);
      setData(data);
      setError(null);
    })
    .catch(err => {
      setLoading(false);
      setError(err);
      setData(null);
    });
}, []);

上面的代码缺陷很多:首先是模板代码多,维护麻烦;第二是容易出错,比如忘记重置相关变量、没考虑竞争等等问题。

而 swr 里面就几行代码:

const fetcher = (url) => fetch(url).then(res => res.json());
// useSwr(apiKey, fetcher, options)
const { data, isValidating, error  } = useSwr('/api/list', fetcher);

请求先缓存后更新

swr 在发起请求的时候,会优先使用缓存的数据,然后当请求返回的时候更新视图。

为什么这个功能很重要呢?还是用上面那个列表接口作为例子,当用户第二次请求接口的时候(比如从某个页面返回),直接就能看到列表里的数据,然后过一会刷新成最新数据,在体验上就感觉很流畅

如果某个接口更新不是很频繁,那么第二次请求的结果还是原来的数据,用户根本感觉不到有请求的过程,这就很爽😄。

还有个额外的好处,当用户返回之前的页面时,浏览器还能保留滚动位置

swr

请求依赖

条件加载,或者依赖加载:当前一个数据没有准备好的时候,就不会进行请求。这样就不用进行一堆条件判断了。

const { data: user } = useSWR('/api/user')
// useSwr(apiKey, fetcher, options)
// apiKey 可以是函数
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)

原理是当请求里面的函数抛错的时候,就不会继续请求。

gnosis23 commented 2 years ago

hash算法

useSWR 的第一个参数作为本地缓存的 key 值,相同的 key 会访问到相同的缓存

// key可以是字符串
const { data } = useSWR('/api/user');

// key也可以是函数
const { data } = useSWR(() => '/api/user')

// key也可以是 object 或者 array
const { data } = useSWR(['/api/user', 'test', 1], fetcher);

里面用了一套 hash 算法,可以 hash 基本类型、数组、字符串等等,比如

['/api/user'] => '@"/api/user",'
[{ x: 1 }] => '@#x:1,,'
// 嵌套的 object,递归调用 hash 算法
[{ x: { y: 2 }, z: 3, u: {v: 4} }] => '@#z:3,x:#y:2,,u:#v:4,,,'

而对于一些无法序列的值,如 () => {}, class {},用一个 id 替代,然后用 WeakMap 存储 object -> id 的索引

gnosis23 commented 2 years ago

关于请求的一些特殊用例

组件已卸载

当请求返回后,组件已经被卸载了,这时候 setState 会显示警告。swr里面用了一个 unmountedRef 来保护

const unmountedRef = useRef(false)

const revalidate = useCallback((...) => {
  if (!unmountedRef.current) return;
}, []);

useIsomorphicLayoutEffect(() => {
  unmountedRef.current = false

  return () => {
    unmountedRef.current = true
  }
}, [key, revalidate]);

请求dedupe

假设两个组件里都发出相同的请求(key相同),那么就不应该发两个请求。

swr里面用了 2 个 map 来保存相同引用,一个用来存储 fetcher 返回的 promise,另一个用来存储 fetcher 发送的时间戳。 至于为什么要搞个时间戳,是因为有时需要请求去重dedupe,有时不需要去重(手动mutate)。

const revalidate = useCallback((...) => {
  // ...
  if (shouldStartNewRequest) {
    CONCURRENT_PROMISES_TS[key] = getTimestamp();
    CONCURRENT_PROMISES[key] = fn(...fnArg);
  }

  if (CONCURRENT_PROMISES_TS[key] !== startAt) {
    // ...
    return false;
  }
}, []);
gnosis23 commented 2 years ago

失败重连

请求发送失败时,自动重连,默认8次

gnosis23 commented 1 year ago

依赖收集

只有收集的依赖变化了以后,才会触发组件重新渲染

return {
  trigger,
  reset,
  get data() {
    // 使用 get 收集依赖
    stateDependencies.data = true
    return currentState.data
  },
  get error() {
    stateDependencies.error = true
    return currentState.error
  },
  get isMutating() {
    stateDependencies.isMutating = true
    return currentState.isMutating
  }
}
const setState = useCallback(
  (payload: Partial<S>) => {
    let shouldRerender = false

    const currentState = stateRef.current
    for (const _ in payload) {
      const k = _ as keyof S

      // If the property has changed, update the state and mark rerender as
      // needed.
      if (currentState[k] !== payload[k]) {
        currentState[k] = payload[k]

        // If the property is accessed by the component, a rerender should be
        // triggered.
        // 收集依赖后才会触发
        if (stateDependenciesRef.current[k]) {
          shouldRerender = true
        }
      }
    }

    if (shouldRerender && !unmountedRef.current) {
      if (IS_REACT_LEGACY) {
        rerender({})
      } else {
        ;(React as any).startTransition(() => rerender({}))
      }
    }
  },
  [rerender]
)
gnosis23 commented 1 year ago

Suspense 模式

非官方:throw 一个 promise 可以让 suspense 转为 fallback 。

// change window.a to 1 
const Mock = () => {
  const a = useSyncExternalStore(
    (cb) => { setInterval(cb, 1000) },
    () => window.a,
  );
  if (!a) throw new Promise.resolve(true);
  return <div>{a}</div>
}

// b
(
  <Suspense fallback={<div>loading</div>}>
    <Mock />
  </Suspense>
)

后续 React 会出一个 use 来支持,上面的方法是不稳定的。