React Hooks使用常见的坑

React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。

为什么会有Hooks?

介绍Hooks之前,首先要给大家说一下React的组件创建方式,一种是类组件,一种是纯函数组件,并且React团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。也就是说组件的最佳写法应该是函数,而不是类。。

函数组件比类组件更加方便实现业务逻辑代码的分离和组件的复用,函数组件也比类组件轻量,没有react hooks之前,函数组件是无法实现LocalState的,这导致有localstate状态的组件无法用函数组件来书写,这限制了函数组件的应用范围,而react hooks扩展了函数组件的能力。可是在使用的过程中,也要注意下面这些问题,否则就会掉进坑里,造成性能损失。按照下面的方法做,,才能避开这些陷阱。

1. 将与状态改变无关的变量和方法提取到组件函数外面

每次状态改变时,整个函数组件都会重新执行一遍。导致函数组件内部定义的方法和变量,都会重新创建,重新给它们分配内存,这会导致性能受到影响。

import React, {useState,useCallback} from "react";

// 测试每次状态改变时,方法是不是重新分配内存
let testFooMemoAlloc = new Set();

const Page = (props:any) => {
  console.log('每次状态改变,函数组件从头开始执行')
  const [count, setCount] = useState(0);
  const calc = () => {
    setCount(count + 1);
  }

  const bar = {
    a:1,
    b:2,
    c: '与状态无关的变量定义'
  }

  const doFoo = () => {
    console.log('与状态无关的方法');

  }
  testFooMemoAlloc.add(doFoo)

  return (
    <>
      <button onClick={calc}>加1</button>
      <p>count:{count}</p>
      <p>testFooMemoAlloc.size增加的话,说明每次都重新分配了内存:{testFooMemoAlloc.size}</p>
    </>
  )
}

export default Page;

与改变状态相关的变量和方法,必须放在hooks组件内,而无状态无关的变量和方法,可以提取到函数组件外,避免每次状态更新,都重新分配内存。也可以分别使用useMemo和useCallback包裹变量与函数,也能达到同样的效果,后面会讲。

import React, {useState,useCallback} from "react";

// 测试每次状态改变时,方法是不是重新分配内存
let testFooMemoAlloc = new Set();

const bar = {
  a:1,
  b:2,
  c: '与状态无关的变量定义'
}

const doFoo = () => {
  console.log('与状态无关的方法');

}

const Page = (props:any) => {
  console.log('每次状态改变,函数组件从头开始执行')
  const [count, setCount] = useState(0);
  const calc = () => {
    setCount(count + 1);
  }

  testFooMemoAlloc.add(doFoo)

  return (
    <>
      <button onClick={calc}>加1</button>
      <p>count:{count}</p>
      <p>testFooMemoAlloc.size增加的话,说明每次都重新分配了内存:{testFooMemoAlloc.size}</p>
    </>
  )
}

export default Page;

2. 用memo对子组件进行包装

父组件引入子组件,会造成一些不必要的重复渲染,每次父组件更新count,子组件都会更新。

import React,{useState} from "react";
const Child = (props:any) => {
    console.log('子组件?')
    return(
        <div>我是一个子组件</div>
    );
}
const Page = (props:any) => {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={(e) => { setCount(count+1) }}>加1</button>
            <p>count:{count}</p>
            <Child />
        </>
    )
}

export default Page;

使用memo,count变化子组件没有更新

import React,{useState,memo} from "react";
const Child = memo((props:any) => {
    console.log('子组件?')
    return(
        <div>我是一个子组件</div>
    );
})
const Page = (props:any) => {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={(e) => { setCount(count+1) }}>加1</button>
            <p>count:{count}</p>
            <Child />
        </>
    )
}

export default Page;

给memo传入第二个参数,开启对象深度比较。当子组件传递的属性值未发生改变时,子组件不会做无意义的render。

memo不仅适用于函数组件,也适用于class组件,是一个高阶组件,默认情况下只会对复杂对象做浅层比较,如果想做深度比较,可以传入第二个参数。与shouldComponentUpdate不同的是,deepCompare返回true时,不会触发 render,如果返回false,则会。而shouldComponentUpdate刚好与其相反。

import React, {useState, memo } from "react";
import deepCompare from "./deepCompare";

const Child = memo((props:any) => {
    console.log('子组件')
  return (
      <>
      <div>我是一个子组件</div>
      <div>{ props.fooObj.a}</div>
      </>
    );
}, deepCompare)

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const [fooObj, setFooObj] = useState({ a: 1, b: { c: 2 } })
  console.log('页面开始渲染')
  const calc = () => {
    setCount(count + 1);
    if (count === 3) {
      setFooObj({ b: { c: 2 }, a: count })
    }
  }
  const doBar = () => {
    console.log('给子组件传递方法,测试一下是否会引起不必须的渲染')
  }
    return (
        <>
        <button onClick={calc}>加1</button>
        <p>count:{count}</p>
        <Child fooObj={fooObj} doBar={doBar} />
        </>
    )
}

export default Page;
// 深度比较两个对象是否相等
export default function deepCompare(prevProps: any, nextProps: any) {
  const len: number = arguments.length;
  let leftChain: any[] = [];
  let rightChain: any = [];
  // // console.log({ arguments });
  //
  if (len < 2) {
    // console.log('需要传入2个对象,才能进行两个对象的属性对比');
    return true;
  }
  // for (let i = 1; i < len; i++) {
  // leftChain = [];
  // rightChain = [];
  console.log({ prevProps, nextProps });
  if (!compare2Objects(prevProps, nextProps, leftChain, rightChain)) {
    // console.log('两个对象不相等');
    return false;
  }
  // }
  // console.log('两个对象相等');

  return true;
}

function compare2Objects(prevProps: any, nextProps: any, leftChain: any, rightChain: any) {
  var p;

  // 两个值都为为NaN时,在js中是不相等的, 而在这里认为相等才是合理的
  if (isNaN(prevProps) && isNaN(nextProps) && typeof prevProps === 'number' && typeof nextProps === 'number') {
    return true;
  }

  // 原始值比较
  if (prevProps === nextProps) {
    console.log('原始值', prevProps, nextProps);
    return true;
  }

  // 构造类型比较
  if (
    (typeof prevProps === 'function' && typeof nextProps === 'function') ||
    (prevProps instanceof Date && nextProps instanceof Date) ||
    (prevProps instanceof RegExp && nextProps instanceof RegExp) ||
    (prevProps instanceof String && nextProps instanceof String) ||
    (prevProps instanceof Number && nextProps instanceof Number)
  ) {
    console.log('function', prevProps.toString() === nextProps.toString());
    return prevProps.toString() === nextProps.toString();
  }

  // 两个比较变量的值如果是null和undefined,在这里会退出
  if (!(prevProps instanceof Object && nextProps instanceof Object)) {
    console.log(prevProps, nextProps, 'prevProps instanceof Object && nextProps instanceof Object');
    return false;
  }

  if (prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)) {
    console.log('prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)');
    return false;
  }

  // 构造器不相等则两个对象不相等
  if (prevProps.constructor !== nextProps.constructor) {
    console.log('prevProps.constructor !== nextProps.constructor');
    return false;
  }

  // 原型不相等则两个对象不相等
  if (prevProps.prototype !== nextProps.prototype) {
    console.log('prevProps.prototype !== nextProps.prototype');
    return false;
  }

  if (leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1) {
    console.log('leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1');
    return false;
  }

  // 遍历下次的属性对象,优先比较不相等的情形
  for (p in nextProps) {
    if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
      console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
      return false;
    } else if (typeof nextProps[p] !== typeof prevProps[p]) {
      console.log('typeof nextProps[p] !== typeof prevProps[p]');
      return false;
    }
  }
  // console.log('p in prevProps');
  // 遍历上次的属性对象,优先比较不相等的情形
  for (p in prevProps) {
    // 是否都存在某个属性值
    if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
      console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
      return false;
    }
    // 属性值的类型是否相等
    else if (typeof nextProps[p] !== typeof prevProps[p]) {
      console.log('typeof nextProps[p] !== typeof prevProps[p]');
      return false;
    }

    console.log('typeof prevProps[p]', typeof prevProps[p]);
    switch (typeof prevProps[p]) {
      // 对象类型和函数类型的处理
      case 'object':
      case 'function':
        leftChain.push(prevProps);
        rightChain.push(nextProps);

        if (!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)) {
          console.log('!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)');
          return false;
        }

        leftChain.pop();
        rightChain.pop();
        break;

      default:
        // 基础类型的处理
        if (prevProps[p] !== nextProps[p]) {
          return false;
        }
        break;
    }
  }

  return true;
}

3.用useCallback对组件方法进行包装

当父组件传递方法给子组件的时候,memo好像没什么效果,无论是用const定义的方法,还在用箭头函数或者bind定义的方法,子组件还是执行了

import React, { useState,memo } from 'react';
//子组件会有不必要渲染的例子
interface ChildProps {
  changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('普通函数子组件')
  return(
      <>
          <div>我是普通函数子组件</div>
          <button onClick={changeName}>普通函数子组件按钮</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('箭头函数子组件')
  return(
      <>
          <div>我是箭头函数子组件</div>
          <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button>
      </>
  );
}
const ArrowMemo = memo(ArrowChild);

const BindChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('Bind函数子组件')
  return(
      <>
          <div>我是Bind函数子组件</div>
          <button onClick={changeName}>Bind函数子组件按钮</button>
      </>
  );
}
const BindMemo = memo(BindChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const name = "test";

  const changeName = function() {
    console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染');
  }

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <ArrowMemo  changeName={()=>changeName()}/>
          <BindMemo  changeName={changeName.bind(null)}/>
          <FunMemo changeName={changeName} />
      </>
  )
}

export default Page;

使用useCallback,参数为[],页面初始渲染后,改变count的值,传递普通函数的子组件不再渲染, 传递箭头函数和bind方式书写的方法的子组件还是会渲染

import React, { useState,memo ,useCallback} from 'react';
//子组件会有不必要渲染的例子
interface ChildProps {
  changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('普通函数子组件')
  return(
      <>
          <div>我是普通函数子组件</div>
          <button onClick={changeName}>普通函数子组件按钮</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('箭头函数子组件')
  return(
      <>
          <div>我是箭头函数子组件</div>
          <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button>
      </>
  );
}
const ArrowMemo = memo(ArrowChild);

const BindChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('Bind函数子组件')
  return(
      <>
          <div>我是Bind函数子组件</div>
          <button onClick={changeName}>Bind函数子组件按钮</button>
      </>
  );
}
const BindMemo = memo(BindChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const name = "test";

  const changeName = useCallback(() => {
    console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染');
  },[])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <ArrowMemo  changeName={()=>changeName()}/>
          <BindMemo  changeName={changeName.bind(null)}/>
          <FunMemo changeName={changeName} />
      </>
  )
}

export default Page;

4.用useMemo对组件中的对象变量进行包装

在子组件使用了memo,useCallback的情况下,给子组件传递一个对象属性,对象值和方法都未发生改变的情况下,父组件无关状态变更,子组件也会重新渲染。

import React, { useState,memo ,useCallback} from 'react';
//子组件会有不必要渲染的例子-使用了memo,useCallback的情况下,给子组件传递一个对象属性值
interface ChildProps {
  childStyle: { color: string; fontSize: string;};
  changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
  console.log('普通函数子组件')
  return(
      <>
          <div style={childStyle}>我是普通函数子组件</div>
          <button onClick={changeName}>普通函数子组件按钮</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const childStyle = {color:'green',fontSize:'16px'};

  const changeName = useCallback(() => {
    console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染');
  },[])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <FunMemo childStyle={childStyle} changeName={changeName} />
      </>
  )
}

export default Page;

使用useMemo可以解决给子组件传递对象属性时的不必要更新问题。

import React, { useState,memo, useMemo, useCallback} from 'react';
//子组件会有不必要渲染的例子
interface ChildProps {
  childStyle: { color: string; fontSize: string;};
  changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
  console.log('普通函数子组件')
  return(
      <>
          <div style={childStyle}>我是普通函数子组件</div>
          <button onClick={changeName}>普通函数子组件按钮</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");
  const childStyle = {color:'green',fontSize:'16px'};

  const changeName = useCallback(() => {
    setName('变一下名称')
  }, [])
  const childStyleMemo = useMemo(() => {
    return {
      color: name === '变一下名称' ? 'red':'green',
      fontSize: '16px'
    }
  }, [name])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <FunMemo childStyle={childStyleMemo} changeName={changeName} />
      </>
  )
}

export default Page;

以上就是React Hooks使用避坑指南的详细内容,更多关于React Hooks使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • 使用hooks写React组件需要注意的5个地方

    Hook是React16.8开始新增的特性.虽然React官方文档已经作出了针对React hooks的相关概念的讲解,但是光看官方文档是很难将hooks使用好的,在编写hooks的过程中很容易跳进陷阱和错误.本文总结了5个不好的地方. 01.不需要render的场景下使用useState 在函数组件中我们可以使用useState来管理状态,这使得对状态的管理变得很简单,但是也容易被滥用,我们通过下面的代码样例看下容易忽略的地方. 不推荐× function ClickButton(props)

  • React Hooks使用常见的坑

    React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性.React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题. 为什么会有Hooks? 介绍Hooks之前,首先要给大家说一下React的组件创建方式,一种是类组件,一种是纯函数组件,并且React团队希望,组件不要变成复杂的容器,最好

  • React Hooks的深入理解与使用

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? --拥有了Hooks,生命周期钩子函数可以先丢一边了. 你在还在为组件中的this指向而晕头转向吗? --既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this. 这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张.如果你也对react

  • React 使用Hooks简化受控组件的状态绑定

    开始之前 阅读本文需要对以下几项有一定了解 ECMAScript 6 文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值.剩余参数.展开语法.箭头函数等. Hooks React 在 16.8 版本中推出了 Hooks,它允许你在"函数组件"中使用"类组件"的一些特性. React 本身提供了一些 Hooks,比如 useState.useReducer 等.通过在一个以"use"作为命名起始的函数中调用这些 Hooks,就得到了一个

  • React Hooks常用场景的使用(小结)

    前言 React 在 v16.8 的版本中推出了 React Hooks 新特性.在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处: 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护: 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks

  • 详解如何使用React Hooks请求数据并渲染

    前言 在日常的开发中,从服务器端异步获取数据并渲染是相当高频的操作.在以往使用React Class组件的时候,这种操作我们已经很熟悉了,即在Class组件的componentDidMount中通过ajax来获取数据并setState,触发组件更新. 随着Hook的到来,我们可以在一些场景中使用Hook的写法来替代Class的写法.但是Hook中没有setState.componentDidMount等函数,又如何做到从服务器端异步获取数据并渲染呢?本文将会介绍如何使用React的新特性Hook

  • React Hooks与setInterval的踩坑问题小结

    目录 一.需求 二.解决方案 1.函数式更新 2.使用useRef 3.用useReducer 4.自定义的hooks 一.需求 我们希望有一个每一秒自动+1的定时器 function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval

  • 30分钟精通React今年最劲爆的新特性——React Hooks

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? --拥有了Hooks,生命周期钩子函数可以先丢一边了. 你在还在为组件中的this指向而晕头转向吗? --既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this. 这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张.如果你也对react

  • 使用react的7个避坑案例小结

    React是个很受欢迎的前端框架.今天我们探索下React开发者应该注意的七个点. 1. 组件臃肿 React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是. 当然,我们现在讨论的是React 在React中,我们可以创建一个很多内容的组件,来执行我们的各种任务,但是最好是保证组件精简 -- 一个组件关联一个函数.这样不仅节约你的时间,而且能帮你很好地定位问题. 比如下面的TodoList组件: // ./components/TodoList.j

  • ahooks正式发布React Hooks工具库

    目录 起因 解法 共建 项目目标 品牌升级 社区开源 API 规范 示例演示 开发迭代 下一步 起因 从 React Hooks 正式发布到现在,越来越多的项目正在使用 Function Component 替代 Class Component,Hooks 这一新特性也逐渐被广泛的使用. 然而在实践的过程中,我们发现在很多常见的场景下,大部分逻辑是重复且可被复用的,如对数据请求的逻辑处理,对防抖节流的逻辑处理等,同样的代码经常会在同一个或不同的项目中被重复的编写 . 另一方面,由于 Hooks

  • React前端DOM常见Hook封装示例上

    目录 引言 useEventListener useClickAway useEventTarget useTitle useFavicon 引言 本文是深入浅出 ahooks 源码系列文章的第十四篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 上一篇我们探讨了 ahooks 对 DOM 类 Hooks 使用规范,以及源码中是

  • React前端DOM常见Hook封装示例下

    目录 引言 useFullscreen useHover useDocumentVisibility 引言 本文是深入浅出 ahooks 源码系列文章的第十五篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 上文指路:React前端DOM常见Hook封装示例上 本篇接着针对关于 DOM 的各个 Hook 封装进行解读. useF

  • 使用 React Hooks 重构类组件的示例详解

    目录 1. 管理和更新组件状态 2. 状态更新后的操作 3. 获取数据 4. 卸载组件时清理副作用 5.  防止组件重新渲染 6. Context API 7. 跨重新渲染保留值 8. 如何向父组件传递状态和方法? 9. 小结 最初,在 React 中可以使用 createClass 来创建组件,后来被类组件所取代.在 React 16.8 版本中,新增的 Hooks 功能彻底改变了我们编写 React 程序的方式,使用 Hooks 可以编写更简洁.更清晰的代码,并为创建可重用的有状态逻辑提供了

  • React hooks useState异步问题及解决

    目录 React Hooks useState异步问题 原因 解决方法 React中useState异步更新小坑 问题点 React Hooks useState异步问题 最近在开发中遇到一个问题 我接口请求回来的数据 用useState存储起来. 但是我后面 去改变这个数据的时候每次拿到都是上次的数据没办法及时更新. 原因 useState 返回的更新状态方法是异步的,要在下次重绘才能获取新值.不要试图在更改状态之后立马获取状态. 解决方法 应该使用useRef 存储这个数据,在useEffe

随机推荐