模拟实现 React useDeferredValue

437 阅读3分钟

前言

参考:juejin.cn/post/708346…

签约作者写的就是漂亮,配上源码和动图,让人一看就感觉靠谱,但是呢,我还是没有看太懂,所以,我自己也模拟实现了一个 useDeferredValue 版本。 该作者使用的是 React ,而我使用 React-Native。由于,由于项目任务工期重,没有时间看源码,所以,自己按照自己的理解设计和现实。后续,我再找源码看看。如果先看了源码,可能就会干扰到自己的设计思路了,有利有弊。

典型应用场景

搜索长列表,结果非常多,每输入一个字符就进行一次搜索查找就非常消耗资源,所以,如果有一个延迟反馈的工具,就可以让搜索不会那么频繁执行,从用户角度说连续搜索表明当前并不想进行查询。

设计思路

搜索框,本质就是输入框的改良版本。在 react-native 中,TextInput 进行稍微的修改就可以变身成搜索框。通过 onChangeTextvalue 进行输入字符变更通知和设置。 所以,设计入口在这里:让输入的值慢一点的生效,在输入间隔时间短的时候等待下一次有效字符的输入在进行搜索

核心: 采用定时器 NodeJS.Timeout 作为延迟技术, 使用 useRefuseMemouseCallback 缓存数据和方法。
标准: 间隔时间短的输入认为无效请求,超时之后才是真正生效请求。
接口: 和官方保持一致,不引入额外参数或者改变接口。

具体实现

const DEFER = 500;

type TimeoutType = {
  timeoutId: NodeJS.Timeout;
  cur: number;
};

/**
 * ref: https://juejin.cn/post/7083466010505773093#3
 * !!! This function does not exist before react 18.
 *
 * @param value any type value
 * @param defer timeout (ms)
 * @returns deferred timeout value
 */
export const useDeferredValue = <T,>(value: T, defer: number = DEFER) => {
  const v = versionToArray(React.version);
  if (v[0] && v[0] >= 18) {
    throw new Error('Please use the official version.');
  }

  const _preValue = React.useRef(value);
  const preValue = React.useMemo(() => {
    return _preValue;
  }, []);
  const [_value, setValue] = React.useState(preValue.current);

  const _timeout = React.useRef<TimeoutType>();
  const timeout = React.useMemo(() => {
    return _timeout;
  }, []);

  const _create = React.useCallback(
    (
      defer: number,
      timeout: React.MutableRefObject<TimeoutType | undefined>,
      dispatch: React.Dispatch<React.SetStateAction<T>>,
      value: T,
      preValue: React.MutableRefObject<T>
    ) => {
      if (timeout.current === undefined) {
        timeout.current = {
          timeoutId: setTimeout(() => {
            preValue.current = value;
            timeout.current = undefined;
            dispatch(value);
          }, defer),
          cur: new Date().getTime(),
        };
      }
    },
    []
  );
  const _cancel = React.useCallback(
    (
      defer: number,
      timeout: React.MutableRefObject<TimeoutType | undefined>
    ) => {
      if (timeout.current) {
        const cur = new Date().getTime();
        if (cur <= timeout.current.cur + defer) {
          clearTimeout(timeout.current.timeoutId);
          timeout.current = undefined;
        }
      }
    },
    []
  );

  if (preValue.current === value) {
    return _value;
  }

  _cancel(defer, timeout);
  _create(defer, timeout, setValue, value, preValue);

  return _value;
};

测试代码

import * as React from 'react';
import { Button as RNButton, Text, View } from 'react-native';
import { useDeferredValue } from 'react-native-chat-uikit';

let count = 0;
export default function TestUtil() {
  React.useEffect(() => {}, []);

  const [value, setValue] = React.useState(0);
  const [value2, setValue2] = React.useState(0);

  const useDeferredValueM = React.useCallback(useDeferredValue, [value]);

  type ObjType = {
    name: string;
    age: number;
  };

  const [obj, setObj] = React.useState<ObjType>({ name: 'zs', age: count });

  return (
    <View style={{ marginTop: 100 }}>
      <View>
        <RNButton
          title="compare value"
          onPress={() => {
            setValue(++count);
          }}
        >
          defer value
        </RNButton>
        <Text>{value}</Text>
        <Text>{useDeferredValue(value)}</Text>
      </View>
      <View>
        <RNButton
          title="compare value"
          onPress={() => {
            setValue2(++count);
          }}
        >
          defer value2
        </RNButton>
        <Text>{value2}</Text>
        <Text>{useDeferredValueM(value2)}</Text>
      </View>
      <View>
        <RNButton
          title="compare object value"
          onPress={() => {
            setObj({ name: obj.name, age: obj.age + 1 });
          }}
        >
          defer object value
        </RNButton>
        <Text>{obj.age}</Text>
        <Text>{useDeferredValueM(obj).age}</Text>
      </View>
    </View>
  );
}

实现效果

useDeferredValue2.gif

说明

有兴趣的小伙伴可能对实现里面的内容有兴趣,可亲自尝试和验证,每一句代码都是认真的。

源码

源码


2023.02.15 更新

上面的实现没有问题,但是限制较多,在 callback 方法里面无法使用。所以,有了 改进 方法。

实现思路和原来的一致,区别就是在变量的保存上没有使用 use 系列。

// ref: https://github.com/jashkenas/underscore/blob/b713f5a6d75b12c8c57fb3f410df029497c2a43f/modules/throttle.js

type Function = (...args: any[]) => any;

// A (possibly faster) way to get the current timestamp as an integer.
function now() {
  return new Date().getTime();
}

// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
export function throttle(
  func: Function,
  wait: number = 500,
  options: any = { leading: false }
) {
  let timeout: NodeJS.Timeout | null, context: any, args: any, result: any;
  let previous = 0;
  if (!options) options = {};

  const later = function () {
    previous = options.leading === false ? 0 : now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  const throttled = function (...argument: any[]) {
    const _now = now();
    if (!previous && options.leading === false) previous = _now;
    const remaining = wait - (_now - previous);
    context = throttled;
    args = argument;
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = _now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function () {
    if (timeout) {
      clearTimeout(timeout);
    }
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
}

源码地址