原创

useRef 进阶

一直以来使用useRef的场景比较常见和基础,大多是为了操作已经mount的dom节点,例如设置焦点之类的,如官方例子所示:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

最近在项目中遇到了使用 useRef 另一种用法的场景,顿时觉得真香,下面我们来分析下该场景~

useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。


场景分析

我们需要在react function component中实现模糊搜索,用户输入过程中触发input组件的onChange事件时获取数据,动态更新下拉列表中的数据项。但是若每次触发onchange事件都去拉取数据,会导致请求太过频繁,前端体验并不好,浪费网络资源的同时还会对后台的服务造成一定的压力,通常这时我们就要使用函数节流 throttle 了。

我们先用class component的写法来还原下:

import React from "react";
import _ from "lodash";

export default class SearchInput extends React.Component {
  state = {
    inputValue: "",
    options: []
  };
  componentDidUpdate(prevProps, prevState) {
    if (prevState.inputValue !== this.state.inputValue) {
      this.updateOptions();
    }
  }
  updateOptions = _.throttle(() => {
    const { options, inputValue } = this.state;
    const list = options.concat([inputValue]);
    this.setState({ options: list });
  }, 500);
  handleChange = e => {
    const { value } = e.target;
    this.setState({ inputValue: value });
  };
  render() {
    const { options } = this.state;
    return (
      <div>
        <input onChange={this.handleChange} />
        <br />
        {options.map((item, i) => {
          return <p key={i}>{item}</p>;
        })}
      </div>
    );
  }
}

节流效果如图:

image.png

没有毛病,那下面我们试试在function component中写:

import React, { useState, useEffect } from "react";
import _ from "lodash";

export default function SearchInput() {
  const [inputValue, setInputValue] = useState("");
  const [options, setOptions] = useState([]);

  useEffect(() => {
    updateOptions();
  }, [inputValue]);

  const updateOptions = _.throttle(() => {
    const list = options.concat([inputValue]);
    setOptions(list);
  }, 500);

  const handleChange = e => {
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input onChange={handleChange} />
      <br />
      {options.map((item, i) => {
        return <p key={i}>{item}</p>;
      })}
    </div>
  );
}

查看下节流效果:

image.png

问题来了,每次输入都触发了options的更新,根本没有节流的效果嘛...

分析后发现,由于react function component的特性,每次渲染都会生成一个新的 updateOptions 方法的副本, 而lodash中的throttled方法默认leading 为 true,即首次触发updateOptions方法时会执行options的更新,这样以来就导致了每次inputValue更新都会更新options,节流就失效啦~

ROUND ONE

既然是因为每次渲染重新生成updateOptions方法的副本导致的,那是不是用useCallback就可以了呢?

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

代码改造如下:

const updateOptions = useCallback(
    _.throttle(() => {
      console.log('options', options);
      const list = options.concat([inputValue]);
      setOptions(list);
    }, 1000),
    []
  );

执行结果如下:

image.png
image.png

看控制台的打印结果,函数节流确实生效了,可是为啥每次从state中获取到的options都是空数组呢?

当然又是因为函数组件的特性了,使用了useCallback之后,updateOptions方法永远是第一次渲染时的版本,其中获取的state也是第一次渲染的副本,没有随着后续组件的重新渲染而更新。

ROUND TWO

整理下我们的预期,我们希望在一个不变的方法里面,获取到可变的值。

听起来有点熟悉,是不是和useRef的官方介绍有点雷同?

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

如果我们把依赖可变state的方法保存在ref.current中,即使ref在组件的整个生命周期内永远不变,但是其current属性却是每一次渲染后更新的值,看起来好像是可行的!

尝试一下,改造部分如下:

const updateRef = useRef(null);
updateRef.current = () => {
  const list = options.concat([inputValue]);
  setOptions(list);
};

const updateOptions = useCallback(
  _.throttle(() => {
    updateRef.current();
  }, 1000),
  []
);

执行结果:

image.png

终于成功了,撒花🎉🎉

后记

虽然功能实现了,但是代码看起来很乱很分散,不加注释的话也不好理解,并且下一次使用函数节流时又得乱写一通,这里能不能抽成一个通用的hooks呢?

// 通用的自定义hooks
export default function useThrottle(func, wait, options) {
  const funcRef = useRef(null);
  funcRef.current = func;

  return useCallback(
    _.throttle(
      () => {
        funcRef.current();
      },
      wait,
      options
    ),
    []
  );
}

// 调用方法
const updateOptions = useThrottle(() => {
    const list = options.concat([inputValue]);
    setOptions(list);
  }, 1000);

搞定!

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • nginx 常见问题记录

    官方文档指明,location if语句中只有实用return 和 rewrite指令是绝对安全的。但是如果某些情况必须使用if 语句进行条件判断怎么办呢?需要...

    CIKEY
  • Elasticsearch介绍

    Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎。

    CIKEY
  • Webassembly初识

    首先,它是一种解释性语言,大神最开始的设计目标用户就是“非专业编程人员和设计师”,避免了非专业人士对编译器了解的需要,解释性语言就是边解释边执行,与编译性语言的...

    CIKEY
  • 手把手教你用神器nextjs一键导出你的github博客文章生成静态html!

    相信有不少小伙伴和我一样用github issues记录自己的blog,但是久而久之也发现了一些小问题,比如

    ssh1995
  • 你应该知道的五种IO模型

    linux操作系统包含了五种IO模型,各种上层编程语言或者网络编程框架的上层实现都是基于操作系统的这些IO实现来实现的。

    春哥大魔王
  • Python书单:涉及 Python 基础、数据分析、机器学习、Web 开发等方向。

    关注我的朋友可能很多都是学习 Python、爬虫、Web、数据分析、机器学习相关的。当然大家可能接触某个方向的时间不一样,可能有的同学已经对某个方向特别精通,有...

    一墨编程学习
  • slim.l2_regularizer()

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

    于小勇
  • Canvas 进阶(二)写一个生成带logo的二维码npm插件

    在前面的文章有讲到如何用 canvas 画二维码,在此基础上再画一个公司logo,并提供下载的方法供调用,再封装成 npm 插件

    小皮咖
  • ASP.NET MVC5+EF6+EasyUI 后台管理系统(67)-MVC与ECharts

    ECharts 特性介绍 ECharts,一个纯 Javascript 的图表库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10...

    用户1149182
  • nodejs微信公众号开发

    网上关于node开发公众号的资料相当缺乏,本文旨在以node的视角对公众号开发做一个阐述。

    一粒小麦

扫码关注云+社区

领取腾讯云代金券