第四期:ahooks全讲解-Part3

220 阅读5分钟

hello,我是海海

这一期我们继续ahooks之旅。阅读时间15分钟。

由于ahooks内容比较多,我将拆分成几个章节,本期我们开始讲解部分副作用相关的hooks

不同于其他介绍ahooks的文章,本系列会对每一个hook从what(是什么)、how(怎么用)、why(实现原理)、when(啥时候用)做全方面的讲解。

欢迎转载,请注明原文和作者

有任何不对的地方,欢迎底部发消息给我。

前文链接:


前置知识:

  • 异步生成器函数

本期大纲:

  • useUpdateEffect
  • useUpdateLayoutEffect
  • useAsyncEffect
  • useDebounceFn
  • useDebounceEffect
  • useLatest
  • useUnmount

异步生成器函数

异步生成器函数 = 异步函数 + 生成器函数

async function* asyncGenerator () {
  yield 'Hello';
  await new Promise(resolve => setTimeout(resolve, 1000));
  yield 'World';
}

(async () => {
  for await (const value of asyncGenerator()) {
  console.log(value);
  }
})()

异步生成器函数会返回一个包含
Symbol.asyncIterator的对象,外部可以通过for await...of对迭代器进行迭代。

说个题外话,可以通过对比普通迭代器和异步迭代器,了解到一些规律。

对比遍历方法哪些对象会返回next是否阻塞迭代器返回值类型
Symbol.iterator
  • for...of
  • iterator.next()
  • ...展开运算符
  • Array.from
  • 生成器函数
  • Array
  • Map
  • Set
  • String字符串
  • TypedArray
  • NodeList对象
  • arguments对象
  • HTMLCollection
阻塞
Symbol.asyncIterator
  • for await...of
  • asyncIterator.next()
  • 通过ix库,使用map/reduce/filter进行遍历
  • 异步生成器函数
  • Streams-API
  • Nodejs-Streams
不阻塞Promise

值得注意的是,for await...of会自动调用异步迭代器的next方法,就如同for...of可以自动调用同步迭代器的next方法一样。

yield能够返回当前的迭代值,并在外部迭代器调用next之前不会继续执行。

useUpdateEffect

忽略首次执行的useUpdateEffect。

import { useEffect } from 'react';
const useUpdateEffect = createUpdateEffect(useEffect)

可以明显的看出来,通过isMounted进行打标,达到忽略首次执行的目的。

用法和使用时机基本等于useEffect。

useUpdateLayoutEffect

忽略首次执行的useLayoutEffect。

useLayoutEffect是react的内置hook,用于在浏览器repaint之前执行一些逻辑。比如计算hover产生的元素放置在合适的位置。

// ahooks的内置方法,用于创建关于副作用的忽略首次执行的高阶hooks
const createUpdateEffect = (hook) => (effect, deps) => {
  const isMounted = false;
  
  hook(() => {
    return () => {
      isMounted.current = false;
    }
  });
  
  hook(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
}

return createUpdateEffect();
import { useLayoutEffect } from 'react';
const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect);

可以明显的看出来,通过isMounted进行打标,达到忽略首次执行的目的。

关于如何使用的问题:入参和
useLayoutEffect相同。

那么在什么时候使用呢?我的建议是在网页重渲染之前做逻辑处理。

useAsyncEffect

支持异步的useEffect

// 用来判断e是否是一个异步迭代器对象
const isAsyncGenerator = (e) => {
  return typeof e[Symbol.asyncIterator] === 'function';
}
const useAsyncEffect = (effect, deps) => {
  useEffect(() => {
    const e = effect();
    let cancelled = false;
    async function execute() {
      if (isAsyncGenerator(e)) {
        while(true) {
          const result = await e.next();
          if (reuslt.done || cancelled) {
            break;
          }
        }
      } else {
        await e;
      }
    }
    execute();
    return () => {
      cancelled = true;
    }
  }, deps);
}

从上面的代码中,我们可以看到:为useEffect添加异步能力,不仅考虑了普通的异步函数,还考虑到了异步生成器函数。

关于异步生成器函数,请参阅前置知识“异步生成器函数”。

useDebounceFn

用来处理防抖函数的hook。

const useDebounceFn = (fn, options) => {
  const fnRef = useLatest(fn);
  const wait = options?.wait ?? 1000;
  const debounced = useMemo(
    () => debounce(
      (...args) => {
        return fnRef.current(...args);
      },
      wait,
      options
    ),
  []);
  
  useUnmount(() => {
    debounced.cancel();
  });
  
  return {
    run: debounced,
    cancel: debounced.cancel,
    flush: debounced.flush
  }
}

和useDebounceEffect的不同点在于,useDebounceFn没有useEffect的功能。只是单纯的做了一层防抖处理。

根据代码,我们可以看到其内部的实现,核心是通过lodash/debounce包装产生防抖函数。那么为什么需要使用useMemo包装呢?

答案是,如果组件触发更新,debounce函数会重新执行,产生不必要的计算浪费。同时,使用useUnmount卸载时,debounced.cancel应当修改为删除上一次,使得清理操作变得更为复杂,因为这可能需要用到全局变量保存上一次的结果。

useDebounceEffect

为useEffect添加防抖能力,会忽略首次执行,并添加了cancel,flush能力。

cancel可以取消,flush可以立即调用。

const useDebounceEffect = (effect, deps, options) => {
  const [flag, setFlag] = useState({});
  
  const { run } = useDebounceFn(() => {
    setFlag({});
  }, options);
  
  useEffect(() => {
    return run();
  }, deps);
  
  useUpdateEffect(effect, [flag]);
}

在看useDebounceEffect的实现之前,我们需要先了解下useEffect的return触发的时机。

useEffect会在组件卸载或者依赖数组更新时会触发return回调函数的执行。

ahooks内部根据依赖数组更新触发return回调的执行的机制,对effect进行包装,默认是1秒内进行防抖。

关于防抖的实现,则是通过内部设置一个flag对象,当在1s内多次更新依赖数组时,执行run函数,只在最后一次依赖更新时才会执行effect函数。这个run函数是通过lodash/debounce返回的。

那么什么时候可以使用呢?这就是考察前端防抖场景的积累了。

  • input实时搜索
  • 窗口尺寸调整
  • 按钮快速连续点击
  • 滚动事件(懒加载)
  • 表单项校验
  • 编辑器自动保存
  • 鼠标移动事件
  • ajax防止重复请求(前端幂等)
  • 性能检测(监控)

useLatest

返回当前最新值。

const useLatest = (value) => {
  const ref = useRef(value);
  ref.current = value;
  
  return ref;
}

官方文档所说的用来解决闭包问题是指state多次调用值不会立即更新,而是使用之前的值的场景。

更具体的就是定时器的更新。count对象被useLatest所引用,当count更新时,useLatest会立即更新,因为他们都是引用值。

何时使用呢?笔者认为可以在你需要立即更新state的情况下使用是最合适的。

useUnmount

组件卸载时执行的逻辑。

const useUnmount = (fn) => {
  const fnRef = useLatest(fn);
  
  useEffect(() => () => {
    fnRef.current();
  }, []);
}

需要注意的是,因为useEffect没有依赖数组,所以只有组件卸载才会触发。

何时使用呢?我的建议是可以聚合多个分散在不同useEffect里的卸载逻辑。


今天的旅程到这里就结束了,谢谢你的陪伴!

感谢你的耐心阅读,如果觉得好的话,请给我一个免费的赞

创作不易,感谢你的支持!

创作不易,感谢你的支持!

本文使用 markdown.com.cn 排版