React 和React Hooks区别及使用

690 阅读15分钟

一、React Hooks的简介

2018年底FaceBook的React小组推出Hooks以来,所有的React的开发者都对它大为赞赏。React Hooks就是用函数的形式代替原来的继承类的形式,并且使用预函数的形式管理state,有Hooks可以不再使用类的形式定义组件了。这时候你的认知也要发生变化了,原来把组件分为有状态组件和无状态组件,有状态组件用类的形式声明,无状态组件用函数的形式声明。那现在所有的组件都可以用函数来声明了。也就是说函数组件也可以有state、ref、生命周期等属性了.

二、为什么要出现hook的概念呢?

因为函数式组件是全局当中一个普通函数,在非严格模式下this指向window,但是react内部开启了严格模式,此时this指向undefined,无法像类式组件一样使用state、ref,函数式组件定义的变量都是局部的,当组件进行更新时会重新定义,也无法存储,所以在hook出现之前,函数式组件有很大的局限性,通常情况下都会使用类式组件来进行代码的编写。

三、原始React继承类组件写法和React Hook的写法有什么区别?

原始React继承类组件写法:

import React, { Component } from 'react';
 
class Example extends Component {
    constructor(props) {
        super(props);
        this.state = { count:0 }
    }
    render() { 
        return (
            <div>
                <p>You clicked {this.state.count} times</p>
                <button onClick={this.addCount.bind(this)}>Chlick me</button>
            </div>
        );
    }
    addCount(){
        this.setState({count:this.state.count+1})
    `}`
}
 
export default Example;

hooks写法:

import React, { useState } from 'react';
const Example=()=>{
    const [ count , setCount ] = useState(0);
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={()=>{setCount(count+1)}}>click me</button>
        </div>
    )
}
export default Example;

四、常用的hook

(1)useState()

useState使函数式组件也能保存状态的一个hook,这个hook的入参是状态的初始值,返回值是一个数组,数组里第一个参数为状态的值,第二个参数为修改状态的方法。

useState做了哪些事呢?

首次渲染:render()------得到Demo组件------调用Demo组件------得到虚拟div------创建真实的div

点击button后:调用setCount(count+1)------再次render()------得到Demo组件------调用Demo组件------得到虚拟div------使用DOM Diff对比------更新真实的div

(2)useEffect

useEffect是函数式组件用来模拟生命周期的hook,可以模拟组件挂载完成、更新完成、即将卸载三个阶段,即componentDidMount、componentDidUpdate、componentWillUnmount。

useEffect的一个参数为函数,表示组件挂载、更新时执行的内容,在函数里再返回一个函数,表示组件即将卸载时调用的函数。

第二个参数为可选项,可传入数组,数组里可以为空,表示不依赖任何状态的变化,即只在组件即将挂载时执行,后续任何状态发生了变化,都不调用此hook。数组里也可以定义一或多个状态,表示每次该状态变化时,都会执行此hook。

useEffect(()=>{
  // 这样模拟的是 componentDidMount
}, [])

useEffect(()=>{
  // 这样模拟的是componentDidMount 以及当count发生变化时执行componentDidUpdate
}, [count])

useEffect(()=>{
  return ()=>{
    // 这样模拟的是 componentWillUnmount
  }
}, [])

useEffect两个注意点

  1. React首次渲染和之后的每次渲染都会调用一遍useEffect函数,而之前我们要用两个生命周期函数分别表示首次渲染(componentDidMonut)和更新导致的重新渲染(componentDidUpdate)。
  2. useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而componentDidMonutcomponentDidUpdate中的代码都是同步执行的。个人认为这个有好处也有坏处吧,比如我们要根据页面的大小,然后绘制当前弹出窗口的大小,如果这时候异步的就不好操作了。

(3)useContext

之前用 class 类创建组件的时候,是用 props 传值的。 那现在使用方法(Function)来声明组件,已经没有了constructor构造函数也就没有了props 的接收,那父子组件的传值就成了一个问题。
于是React Hooks为我们准备了useContext

1.useContext可以帮助我们跨越组件层级直接传递变量,实现共享。

需要注意的是useContextredux的作用是不同的!!!
useContext:解决的是组件之间值传递的问题
redux:是应用中统一管理状态的问题
但通过和useReducer的配合使用,可以实现类似Redux的作用。

import React, { useState, createContext, useContext } from 'react';

const CountContext = createContext(0);
//Counter--子组件
const Counter = () => {
  const count:any = useContext(CountContext); // 一句话就可以得到count
  return (<h2>接收父组件的count值:{count}</h2>);
};

const Example = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>
        You clicked
        {count}
        times
      </p>
      <button type="button" onClick={() => { setCount(count + 1); }}>click me</button>
      //父组件引用子组件
      <CountContext.Provider value={count}>
        <Counter />
      </CountContext.Provider>
    </div>
  );
};

export default Example;

如果子组件不在同一个文件里定义,也可以import子组件,但是需要把CountContext传递给子组件

import Counter from './counter';
 
<CountContext.Provider value={count}>
        <Counter CountContext={CountContext} />
      </CountContext.Provider>`
      
 //子组件从props中获取CountContext
 const Counter = (props:any) => {
  const { CountContext } = props;
  const count:any = useContext(CountContext); // 一句话就可以得到count
  return (<h2>接收父组件的count值:{count}</h2>);
};
      

截屏2021-12-24 15.31.06.png

(4)useRef

useRef和类式组件中createRef用法比较类似,返回一个ref对象,这个对象在函数的整个生命周期都不变

createRef 与 useRef 的区别?

useRef 在 react hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西. 

在一个组件的正常的生命周期中可以大致分为3个阶段:

第一个阶段,useRef与createRef没有差别

第二个阶段,createRef每次都会返回个新的引用;而useRef不会随着组件的更新而重新创建

第三个阶段,两者都会销毁

为什么使用useRef?

如果只使用useState()不同渲染之间无法共享state状态值 useRef 是定义在实例基础上的,如果代码中有多个相同的组件,每个组件的 ref 只跟组件本身有关,跟其他组件的 ref 没有关系。

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

useRef常见用法:

1、 操作当前DOM

用于dom元素或者组件上,通过current属性可以获取到dom元素或者类式组件的实例对象。需要注意的是,无论是useRef还是createRef或者是回调形式、字符串形式的ref,都是不能直接给函数式组件定义的,因为函数式组件的this指向undefined,没有实例对象,只能通过forwardRef定义到函数式组件中的某个dom元素。

// 这样就将传递给函数式组件的ref绑定在了函数式组件内部的input标签上
import React, {
  useRef,
} from 'react';

// 使用函数表达式的方式定义了一个函数式组件
const FocusInput = () => {
  const inputRef:any = useRef(null);
  const getFocus = () => {
    inputRef.current.focus();
  };
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button type="button" onClick={getFocus}>获取焦点</button>
    </div>
  );
};

export default FocusInput;

2、获取表单的输入

import React, {
  useRef,
} from 'react';

const FocusInput = () => {
  const eleRef = useRef<any>(null);

  const handleSubmit = (e:any) => {
    console.log(eleRef.current.value);
    // e.preventDefault();
    alert(eleRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <span>
        姓名:
        <input type="text" ref={eleRef} />
      </span>
      <input type="submit" value="Submit" />
    </form>
  );
};

export default FocusInput;

截屏2021-12-27 11.18.25.png

3、获取子组件的属性或方法

import React, {
  MutableRefObject,
  useState,
  useEffect,
  useRef,
  useCallback,
} from 'react';

interface IProps {
    // prettier-ignore
    label: string,
    cRef: MutableRefObject<any>
}
// 子组件
const ChildInput: React.FC<IProps> = (props) => {
  const { label, cRef } = props;
  const [inputValue, setValue] = useState('');
  const handleChange = (e: any) => {
    const { value } = e.target;
    setValue(value);
  };
  const getValue = useCallback(() => inputValue, [inputValue]);
  useEffect(() => {
    if (cRef && cRef.current) {
      cRef.current.getValue = getValue;
    }
  }, [getValue]);
  return (
    <div>
      <span>
        {label}
        :
      </span>
      <input type="text" value={inputValue} onChange={handleChange} />
    </div>
  );
};
// 父组件
const ParentCom: React.FC = (props: any) => {
  console.log(props);
  const childRef: MutableRefObject<any> = useRef({});
  const handleFocus = () => {
    const node = childRef.current;
    alert(node.getValue());
  };
  return (
    <div>
      <ChildInput label="名称" cRef={childRef} />
      <button type="button" onClick={handleFocus}>获取子组件input的值</button>
    </div>
  );
};

export default ParentCom;

截屏2021-12-27 11.39.59.png

4、通过useImperativeHandle,配合forwardRef,获取子组件的属性

forwardRef:  将父类的ref作为参数传入函数式组件中

React.forwardRef((props, ref) => {})  

  • 创建一个React组件,
  • 这个组件将会接受到父级传递的ref属性,
  • 可以将父组件创建的ref挂到子组件的某个dom元素上,
  • 在父组件通过该ref就能获取到该dom元素
import React, {
  MutableRefObject,
  useState,
  useRef,
  useCallback,
  forwardRef,
  useImperativeHandle,
} from 'react';

// 子组件
const ChildInput = forwardRef((props:any, ref:any) => {
  const { label } = props;
  const [inputValue, setValue] = useState('');
  const handleChange = (e: any) => {
    const { value } = e.target;
    setValue(value);
  };
  const getValue = useCallback(() => inputValue, [inputValue]);
  
  // useImperativeHandle作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
  // 参数1: 父组件传递的ref属性
  // 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
  
  useImperativeHandle(ref, () => ({
    getValue,
  }));
  return (
    <div>
      <span>
        {label}
        :
      </span>
      <input type="text" value={inputValue} onChange={handleChange} />
    </div>
  );
});
// 父组件
const ParentCom = (props: any) => {
  console.log(props);
  const childRef: MutableRefObject<any> = useRef({});
  const handleFocus = () => {
    const node = childRef.current;
    alert(node.getValue());
  };
  return (
    <div>
      <ChildInput label="名称" ref={childRef} />
      <button type="button" onClick={handleFocus}>获取子组件input的值</button>
    </div>
  );
};

export default ParentCom;

(5) useReducer

useReducer相当于是useState的升级版,作用与useState类似,都是用来保存状态,但它的不同点在于可以定义一个reducer的纯函数,来处理复杂数据。 reducer 其实是在下次 render 时才执行的,所以在 reducer 里,访问到的永远是新的 props 和 state

useReducer 返回的 dispatch 函数是自带了 memoize(闭包) 的,不会在多次渲染时改变。所以如果你想同时把 state 作为 context 传递下去,请分成两个 context 来声明。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等.并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

指定初始state:将初始值作为第二个参数传入

const [state, dispatch] = useReducer(reducer, initialArg, init)
// 定义一个处理数据的reducer纯函数
function reducer(prevState, action){
  switch(action.type){
    case 'increment':
      return {...prevState, count: prevState.count + 1 }
    case 'decrement':
      return {...prevState, count: prevState.count - 1 }
    default:
    return prevState
  }
}

// 初始化状态
const [ count, dispatch ] = useReducer(reducer, { count: 0 })
// 修改状态,此时的修改需要派发一个action,让传入的reducer函数进行处理
dispatch({ type: 'increment' })
import React, { useReducer } from 'react';

const init = (initialcount:any) => {
  console.log('initialcount', initialcount);
  return initialcount;
};

function reducer(state:any, action:any) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}
// initialCount可以是传入的,这里测试就定义成全局了
const initialCount = { count: 0 };
const Counter = () => {

//*惰性初始化*:创建函数作为`useReducer`的第三个参数传入,初始的`state`就是函数的参数
//这么做可以将用于计算`state`的逻辑提取到` reducer` 外部,这也为将来对重置 `state` 的 `action `做处理提供了便利
 const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <div>
      Count:
      {' '}
      {state.count}
      <button
        type="button"
          // 重置按钮
        onClick={() => dispatch({ type: 'reset', payload: initialCount })}
      >
        Reset
      </button>
      <button type="button" onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button type="button" onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
};

export default Counter;

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用Object.is 比较算法 来比较 state。)

(6) useCallback

函数式组件中,每一次更新状态,自定义的函数都要进行重新的声明和定义,如果函数作为props传递给子组件,会造成子组件不必要的重新渲染,有时候子组件并没有使用到父组件发生变化的状态,此时可以使用useCallback来进行性能优化,它会为函数返回一个记忆的值,如果依赖的状态没有发生变化,那么则不会重新创建该函数,也就不会造成子组件不必要的重新渲染。

看个例子,没有加useCallback的情况下:

import React, { memo, useState } from 'react';

const ChildComp = memo(({ name, onClick }:any) => {
  console.log('render child-comp ...');
  return (
    <>
      <div>
        child-comp ...
        {name}
      </div>
      <button type="button" onClick={() => onClick('hello')}>改变 name 值</button>
    </>
  );
});
function ParentComp() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  const [name, setName] = useState('hi~');
  const changeName = (newName:any) => setName(newName); // 父组件渲染时会创建一个新的函数

  return (
    <div>
      <button type="button" onClick={increment}>
        点击次数:
        {count}
      </button>
      <ChildComp name={name} onClick={changeName} />
    </div>
  );
}

export default ParentComp;

父组件在调用子组件时传递了 name 属性和 onClick 属性,此时点击父组件的按钮,可以看到控制台中打印出子组件被渲染的信息。 截屏2021-12-27 16.09.49.png

子组件包裹了memo,为什么子组件还是会重复渲染呢? 分析下原因:

  • 点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值),进而导致父组件重新渲染;
  • 父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 onClick 属性发生了变化,导致子组件渲染;

但是我们只是点击了父组件的按钮,并未对子组件做任何操作,压根就不希望子组件的 props 有变化,这个时候就可以用useCallback

解决方法:修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层。

import React, { useCallback } from 'react'

function ParentComp () {
  // ...
  const [ name, setName ] = useState('hi~')
  // 每次父组件渲染,返回的是同一个函数引用
  const changeName = useCallback((newName) => setName(newName), [])  

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

此时点击父组件按钮,控制台不会打印子组件被渲染的信息了。

究其原因:useCallback() 起到了缓存的作用,即便父组件渲染了,useCallback() 包裹的函数也不会重新生成,会返回上一次的函数引用。

(7) React.memo

当数据变化时,代码会重新执行一遍,但是子组件数据没有变化也会执行,这个时候可以使用memo将子组件封装起来,让子组件的数据只在发生改变时才会执行

什么时候用React.memo?

React 中当组件的 props 或 state 变化时,会重新渲染视图,实际开发会遇到不必要的渲染场景。

//子组件
function ChildComp () {
  console.log('render child-comp ...')
  return <div>Child Comp ...</div>
}

//父组件
function ParentComp () {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp />
    </div>
  );
}

上面的例子,子组件中有条 console 语句,每当子组件被渲染时,都会在控制台看到一条打印信息。 点击父组件中按钮,会修改 count 变量的值,进而导致父组件重新渲染,此时子组件压根没有任何变化(props、state),但在控制台中仍然看到子组件被渲染的打印信息。

我们期待的结果应该是:子组件的 props 和 state 没有变化时,即便父组件渲染,也不要渲染子组件

方法:修改子组件,用React.memo()包一层。这种写法是 React 的高阶组件写法,将组件作为函数(memo)的参数,函数的返回值(ChildComp)是一个新的组件。

import React, { memo } from 'react'

const ChildComp = memo(function () {
  console.log('render child-comp ...')
  return <div>Child Comp ...</div>
})

(8) useMemo

useMemo也是返回一个记忆的值,如果依赖的内容没有发生改变的话,这个值也不会发生变化,useMemo与useCallback的不同点在于useMemo需要在传入的函数里需要return 一个值,这个值可以是对象、函数

前面父组件调用子组件时传递的 name 属性是个字符串,如果换成传递对象会怎样?

下面例子中,父组件在调用子组件时传递 info 属性,info 的值是个对象字面量,点击父组件按钮时,发现控制台打印出子组件被渲染的信息。

import React, { useCallback } from 'react'

function ParentComp () {
  // ...
  const [ name, setName ] = useState('hi~')
  const [ age, setAge ] = useState(20)
  const changeName = useCallback((newName) => setName(newName), [])
  const info = { name, age }    // 复杂数据类型属性

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp info={info} onClick={changeName}/>
    </div>
  );
}

分析原因跟调用函数是一样的:

  • 点击父组件按钮,触发父组件重新渲染;
  • 父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 info 属性值变化,进而导致子组件重新渲染。

解决

使用 useMemo 对对象属性包一层。

useMemo 有两个参数:

  • 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
  • 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。
function ParentComp () {
  // ....
  const [ name, setName ] = useState('hi~')
  const [ age, setAge ] = useState(20)
  const changeName = useCallback((newName) => setName(newName), [])
  const info = useMemo(() => ({ name, age }), [name, age]) // 包一层

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <ChildComp info={info} onClick={changeName}/>
    </div>
  );
}

React.memo和useMemo的区别??

  • React.memo

    • 包裹react组件,来自父组件的props没有发生改变的话,就不会渲染子组件
    • 第二个参数,可以传入一个判断方isEqual,可以拿到prePropsprops做比较,返回布尔值,决定是否更新渲染组件
  • useMemo

    • useMemo可以用于处理颗粒度更细的情况,对于组件内的某一部分进行缓存,只有第二个参数更新,才会执行回调,得到最新的变量/组件,否则不变
    • useCallback的原理也是一样的,区别就是,它是为了避免函数重复定义,一种对函数的缓存

(8) useImperativeHandle

这个是与forwardRef配合来使用的,当我们对函数式组件使用forwardRef将ref指定了dom元素之后,那就父组件就可以任意的操作指定的dom元素,使用useImperativeHandle就是为了控制这样的一种行为,指定父元素可操作的子元素的方法。

可以看上上面useRef() 的例子

(9)useLayoutEffect

这个方法与useEffect类似,只是执行的顺序稍有不同,useEffect是在组件渲染绘制到屏幕上之后,useLayoutEffect是render和绘制到屏幕之间。

useEffect和useLayoutEffect区别??

useEffect

基本上90%的情况下,都应该用这个,这个是在render结束后,你的callback函数执行,但是不会block browser painting,算是某种异步的方式吧,但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.

useLayoutEffect

这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

image.png 例子:

import React, { useEffect, useLayoutEffect, useRef } from "react";
import TweenMax from "gsap/TweenMax";
import './index.less';

const Animate = () => {
    const REl = useRef(null);
    useEffect(() => {
        /*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
        TweenMax.to(REl.current, 0, {x: 600})
    }, []);
    return (
        <div className='animate'>
            <div ref={REl} className="square">square</div>
        </div>
    );
};

export default Animate;

8641818-b98fb38e8977d661.webp

使用useLayoutEffect后: 8641818-8b33d7b0d65ccba8.webp

参考文档:

blog.csdn.net/u011705725/… juejin.cn/post/702746… www.cnblogs.com/vigourice/p…