当前位置:编程学习 > > 正文

react hooks详解(React Hooks使用常见的坑)

时间:2021-10-26 11:36:27类别:编程学习

react hooks详解

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;
    
  • react hooks详解(React Hooks使用常见的坑)

    与改变状态相关的变量和方法,必须放在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;
    
  • react hooks详解(React Hooks使用常见的坑)

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

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

  • import React,{useState} from "react";
    const Child = (props:any) => {
        console.log('子组件?')
        return(
            <li>我是一个子组件</li>
        );
    }
    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;
    
  • react hooks详解(React Hooks使用常见的坑)

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

  • import React,{useState,memo} from "react";
    const Child = memo((props:any) => {
        console.log('子组件?')
        return(
            <li>我是一个子组件</li>
        );
    })
    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;
    
  • react hooks详解(React Hooks使用常见的坑)

    给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 (
          <>
          <li>我是一个子组件</li>
          <li>{ props.fooObj.a}</li>
          </>
        );
    }, 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;
    }
    
  • react hooks详解(React Hooks使用常见的坑)

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

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

  • import React, { useState,memo } from 'react';
    //子组件会有不必要渲染的例子
    interface ChildProps {
      changeName: ()=>void;
    }
    const FunChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('普通函数子组件')
      return(
          <>
              <li>我是普通函数子组件</li>
              <button onClick={changeName}>普通函数子组件按钮</button>
          </>
      );
    }
    const FunMemo = memo(FunChild);
    
    const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('箭头函数子组件')
      return(
          <>
              <li>我是箭头函数子组件</li>
              <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button>
          </>
      );
    }
    const ArrowMemo = memo(ArrowChild);
    
    const BindChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('Bind函数子组件')
      return(
          <>
              <li>我是Bind函数子组件</li>
              <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;
    
  • react hooks详解(React Hooks使用常见的坑)

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

  • import React, { useState,memo ,useCallback} from 'react';
    //子组件会有不必要渲染的例子
    interface ChildProps {
      changeName: ()=>void;
    }
    const FunChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('普通函数子组件')
      return(
          <>
              <li>我是普通函数子组件</li>
              <button onClick={changeName}>普通函数子组件按钮</button>
          </>
      );
    }
    const FunMemo = memo(FunChild);
    
    const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('箭头函数子组件')
      return(
          <>
              <li>我是箭头函数子组件</li>
              <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button>
          </>
      );
    }
    const ArrowMemo = memo(ArrowChild);
    
    const BindChild = ({ changeName}: ChildProps): JSX.Element => {
      console.log('Bind函数子组件')
      return(
          <>
              <li>我是Bind函数子组件</li>
              <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;
    
  • react hooks详解(React Hooks使用常见的坑)

    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(
          <>
              <li style={childStyle}>我是普通函数子组件</li>
              <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;
    
  • react hooks详解(React Hooks使用常见的坑)

    使用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(
          <>
              <li style={childStyle}>我是普通函数子组件</li>
              <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使用常见的坑)

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

    标签:
    上一篇下一篇

    猜您喜欢

    热门推荐