React的函数组件及Hooks

2,665 阅读15分钟

React的函数组件及Hooks

本章主要介绍了React函数组件的创建和各类Hook的使用以及使用技巧。

函数组件

  • 函数组件的创建

    // 传统函数
    function Hello(props){
        return (<div>Hello world</div>)
    }
    // 箭头函数
    const Hello = (props)=>{
        return (<div>Hello world</div>)
    }
    // 仅返回模板时,箭头函数适当简写
    const Hello = props => <div>Hello world</div>
    

    函数组件的创建要注意2点:传propsreturn的内容用()包裹起来

  • 函数组件使用props、state及内部函数声明

    请回看React组件基础。

  • 函数组件与类组件的对比

    比起类组件,函数组件的代码量更小,代码更简洁,更简单,容易阅读。更重要的是,使用函数组件能够避免复杂的this指向的问题

    那函数组件有缺点吗?在ReactV16.8.0之前,函数组件的致命缺点在于:没有state和生命周期。但是在eactV16.8.0之后,React推出了Hooks,开发者利用Hooks就能够实现state和生命周期的功能。

    下面就让我们深入学习各个Hooks的使用吧。

React Hooks

Hooks的引入

如果引入了React那么就可以直接使用React.hooksName来调用hooks。同时我们也能直接引入特定的Hooks,直接调用。

// 以使用useState为例
import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  // 引入useState,直接调用useState
  const [user,setUser] = useState({name:'Frank', age: 18})
  // 引入React,通过React来调用useState
  const [user2,setUser2] = React.useState({name:'Jack', age: 18})
  return ( <div className="App"></div> );
}

useState

  • 作用:

    useState主要是用于创建state的读、写的接口。

  • 使用useState创建state:

    function App() {
      // 初始值传入一个简单类型
      const [n,setN] = useState(0)
      // 初始值传入一个对象
      const [user,setUser] = useState({name:'Frank', age: 18})
      // 传入一个函数,用函数的返回值作为初始值
      const [m,setM] = useState( ()=>{return 0 })
      return ( <div className="App"></div> );
    }
    
  • 使用细节:

    1. 初次渲染,useState返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。而在后续的渲染中,useState 返回的第一个值将始终是更新后最新的 state。

    2. 写接口(setState)不能局部更新数据,只能完全覆盖原来的state对象。而且不像类组件一样,函数组件不会合并属性。我们使用写接口更新数据时,要记得通过展开语法先引入原来的所有state数据,再进行数据的修改,如:setState({...state,name:'jack'})

    3. 在使用写接口时,我们可以直接传入数据,同时我们还能够通过传入函数来修改state,传入的函数接收先前的 state ,并返回一个更新后的值 。这两个的区别在于:通过传入函数来修改state,我们可以连续使用多个setState来对state进行多次的修改。推荐尽量通过传入函数来使用setState

      // 每个按钮点击一次,结果 n=2,m=3
      function App() {
        const [n, setN] = useState(0);
        const [m, setM] = useState(0);
       const onClick = () => {
          setN(n + 1);
          console.log(n); // 0
          setN(n + 2); 
        };
        const onClick2 = () => {
          setM(i => i + 1);
          setM(i => {
            console.log(i); // 1
            return i + 2;
          });
        };
        return (
          <div className="App">
            <h1>n: {n}</h1>
            <button onClick={onClick}>+2</button>
            <h1>m: {m}</h1>
            <button onClick={onClick2}>+2</button>
          </div>
        );
      }
      

      针对上面的例子:n为什么结果为2不等于3?m的结果为3?

      归根结底,是因为setState参数传入的时机的问题

      首先,setState()执行后,会马上将组件的一次重新渲染加入队列。但是setState并不会马上执行,而是加入执行队列,等待主线程的代码执行完毕后才会执行。setN加入执行队列的时候,参数就已经传入了,实际上先执行了setN(0+1)后执行了setN(0+2),而第2次setN的结果覆盖了第1次的结果。而setM加入执行队列的时候,参数是一个函数,这个参数具体的值只有在对应的setM真正调用的时候才会确定。第1次setM的时候,m为0,所以传入的state为0。而第2次setM的时候,m为1,所以实际上执行的是setM(1=>1+2)

    4. 使用写接口更新对象类型的数据时,一定要注意更新对象的地址,否则React会认为state并没有变化。因此React不会触发组件的重新渲染。React检查state中某对象的数据变没变,是通过这个对象的地址是否改变来决定的(复杂类型看地址,简单类型看值)。

      function App() {
          const [user,setUser]= React.useState({ name:'jack'})
          const onClick = ()=>{
              user.name = 'Ben'; 
              setUser(user);
          }
      }
      

      上面的代码执行onClick后不会触发组件的重新渲染,你会发现页面中的渲染的user.name仍然是'jack'。

    useReducer

    • 作用:

      作用和useState很像,是useState的替代方法。useReducer 可以将函数内部声明的state和操作state的方法抽离出来,单独声明集中处理。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

    • 使用:

      下面是一个点击按钮,+1、+2的实例:

      // 这里我们通过一个函数来存储需要初始化的state的值,后续会传给useReducer
      // 当然,也可以省略这个步骤,直接在useReducer中声明一个对象
      const initial = {
        n: 0
      };
      
      // 声明reducer,后续会传给useReducer
      const reducer = (state, action) => {
        // 通过action.type 类型的不同选择触发不同的处理函数,对state进行不同的操作并返回
        // 同时还能在调用dispatch时动态传入参数,这里先声明 action.number,后续调用时才传入具体的值
        if (action.type === "add") {
          return { n: state.n + action.number };
        } else if (action.type === "multi") {
          return { n: state.n * 2 };
        } else {
          throw new Error("unknown type");
        }
      };
      
      function App() {
        // 使用useReducer,传入声明好的 reducer 和 initial
        // 并用 [state, dispatch] 接受返回的当前的state和dispatch
        const [state, dispatch] = useReducer(reducer, initial);
        const { n } = state;
        const onClick = () => {
          // 调用dispatch,操作state
          // action.type必须传入,其它的参数可以根据使用情况选择性传入
          dispatch({ type: "add", number: 1 });
        };
        const onClick2 = () => {
          dispatch({ type: "add", number: 2 });
        };
        return (
          <div className="App">
            <h1>n: {n}</h1>
            <button onClick={onClick}>+1</button>
            <button onClick={onClick2}>+2</button>
          </div>
        );
      }
      

      上面的例子中比较关键的地方在于声明reducer时第2个参数action的使用。action是一个对象,通过action中的type属性,可以选择触发的函数,对state进行不同的操作。同时action中还能声明其它属性,如:action.number,在后续的调用时动态传入具体的值,根据不同的要求传入不同的值,实现一个函数,多重效果。

    • 使用细节:

      1. 虽然useReducer使用起来比useState复杂,但是在一些复杂的情况下,会更具性价比,以及代码会更加清晰。
      2. useReducer()返回的内容,一般都用 [state, dispatch] 接受,当然也可以使用其他名字,但不推荐。
      3. useReducer的执行只能写在函数组件的内部,可以在组件外部声明函数调用useReducer,但是useReducer调用的时机一定是在函数组件内部。
      4. 可以使用useReducer来替代Redux

    useContext

    • 作用:

      局部实现一个类似全局作用域的东西,在该区域中的所有组件都能够通过useContext直接使用预设好的一些变量,不需要传入。

    • 使用:

      // 创建上下文
      const C = React.createContext(null);
      
      function App() {
        const [n, setN] = useState(0);
        return (
          // <C.Provider>圈定作用域范围,并传入需要全局使用的value
          <C.Provider value={{ n, setN }}>
            <div className="App">
              <Baba />
            </div>
          </C.Provider>
        );
      }
      
      function Baba() {
        // 使用 useContext 来获取需要操作的数据
        const { n, setN } = useContext(C);
        return (
          <div>
            <!-- 使用获取的数据n -->
            我是爸爸 n: {n} <Child />
          </div>
        );
      }
      
      function Child() {
        // 使用 useContext 来获取需要操作的数据
        const { n, setN } = useContext(C);
        const onClick = () => {
          // 使用获取的方法 setN
          setN(i => i + 1);
        };
        return (
          <div>
            <!-- 使用获取的数据n -->
            我是儿子 我得到的 n: {n}
            <button onClick={onClick}>+1</button>
          </div>
        );
      }
      
      1. 使用const ContextName = React.createContext(defaultValue)创建上下文React.createContext支持传入一个默认值,如果不需要则可以传入null
      2. 使用<ContextName.Provider value={...}>圈定作用域,并通过value传入值。圈定的作用域内部的所有组件,都能通过useContext(ContextName)来获取value中的值。如果不使用<ContextName.Provider value={...}>圈定作用域,并通过value传入值的话,组件通过useContext(ContextName)则可以获取到创建 ContextName 时传入的默认值。
      3. 作用域内部的组件通过useContext(ContextName)获取传递的值或默认值
    • 使用细节:

      1. useContext并非响应式的。即,我们传入的需要全局使用的数据,如果被某个组件内部修改了,其他组件并不会知晓这个变化。

    useEffect

    • 作用:

      在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。而useEffect就是为了完成这些带有副作用的操作。

    • 使用:

      function App(){
          const [n,setN] = useState(0)
          const [m,setM] = useState(0)
          useEffect(()=>{
              const time = setTimeout(()=>{
                  console.log(n)
              },3000)
              return ()=>{
                  clearTimeout(time) 
              }
          },[n])
      }
      

      useEffect()的2个参数

      第一个参数:类型为函数。在满足一定条件下执行的函数,该函数还可以返回一个函数。React称之为effect。

      第二个参数:可选,类型为包含某些变量的数组。数组中的每个变量都是effect的依赖。

      effect的执行

      • effect默认当组件初次渲染后执行。
      • 当组件再次渲染后,effect会根据第二个参数中的依赖选择性执行
        1. 不传参数,effect默认监听组件中的所有变量,即组件后续的每次渲染后effect都会执行。
        2. 参数为[],即不监听任何变量,effect只会在初次渲染执行,组件后续的渲染effect都不会执行。
        3. 参数为[n],即监听state n,组件后续的渲染effect只会在n改变时执行。
      • effect返回的函数会在组件销毁前执行。
    • 使用技巧:

      1. effect的执行是在组件渲染完毕之后才执行。

      2. 一个组件内可以存在多个useEffect,执行顺序则按照代码顺序执行。

      3. 可以使用useEffect来模拟类组件中的:componentDidMount,componentDidUpdate,componentWillUnmount 三个生命周期。

      4. 不要在useEffect的effect中操作DOM。

useLayoutEffect

  • 作用:

    和useEffect类似,但是effect的执行时机和useEffect不同。useLayoutEffect的effect一般会操作DOM元素,那么页面渲染时就不会出现闪烁的效果。

  • 使用:

    所有参数都和useEffect相同。

    const BlinkyRender = () => {
      const [value, setValue] = useState(0);
    
      useLayoutEffect(() => {
        document.querySelector('#x').innerText = `value: 1000`
      }, [value]);
    
      return (
        <div id="x">value: {value}</div>
      );
    };
    
  • 使用细节:

    1. effect的执行时机:

    2. 在使用上,一般来说在操作DOM元素(Layout)时才会使用useLayoutEffect。但是在实际使用上一般很少回去操作DOM,所以 useLayoutEffect使用频率较少,因为会对UI的渲染造成延迟。

    3. 如果组件中同时存在useLayoutEffect与useEffect,那么useLayoutEffect总是先执行。

    4. 尽量使用useEffect而不是useLayoutEffect。

useMemo

  • 作用:

    useMemo会根据传入的函数返回一个 memoized 值,这个 memoized 可以是任何类型的数据,这个 memoized 会缓存下来,只有依赖项改变后才会重新计算。如过不传递依赖项,则 memoized 中任何的变动都会触发重新计算。可以参考Vue的computed。通常会用于减少子组件的无意义渲染

  • 使用:

    代码示例:const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

    useMemo接受一个具有返回值的函数,以及memoized依赖项的数组,数组中的依赖项改变后会重新计算memoized。如过不传递依赖项,则 memoized 中任何的变动都会触发重新计算。

    function App() {
      const [n, setN] = React.useState(0);
      const [m, setM] = React.useState(0);
      const onClick = () => {
        setN(n + 1);
      };
    
      return (
        <div className="App">
          <div>
            <button onClick={onClick}>update n {n}</button>
          </div>
          <Child data={m} number={1} /> 
          <Child2 data={m} number={2} />
        </div>
      );
    }
    
    function Child(props) {
      console.log(`child ${props.number}执行了`);
      console.log("假设这里有大量代码");
      return <div>child: {props.data}</div>;
    }
    
    const Child2 = React.memo(Child);
    

    上面的实例中,useMemo接受的是一个函数组件(Child),并使用Child2接收。实例中点击按钮,触发setN(n+1),父组件App重新渲染,此时正常情况下子组件也会重新渲染。但是我们使用了useMemo对其中一个子组件进行了缓存,并存储在Child2变量中,由于缓存的子组件中的依赖(props,state,事件函数)并没有改变,所以组件Child2并不会触发再次渲染。点击按钮,在控制台中log了child 1执行了假设这里有大量代码,这就说明了只有第一个Child组件触发了重新渲染。

  • 使用技巧:

    1. useMemo能够优化组件没有意义的渲染,从而提高性能。

    2. 先编写在没有 useMemo 的情况下也可以执行的代码 , 之后再在你的代码中添加 useMemo,以达到优化性能的目的

    3. React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。

    4. 我们传过来的props不仅仅是简单类型,也有可能是对象,比如说:传过来一个数组,或者一个函数。此时当父组件重新执行后,这些对象虽然值还是原来的值,但是地址已经改变了,那么 memoized 也会重新计算。如:<Child2 onclick={clickMe}>,由于父组件再次渲染时声明的 clickMe 内容没有改变,但是地址已经改变,所以Child2组件会触发再次渲染。

      对此我们可以将函数或者其他对象缓存起来:

      const [m, setM] = React.useState(0);
      const onClickChild = useMemo(() => {
          const fn = () => {
            console.log("on click child, m: " + m);
          };
          return fn;
        }, [m]); // 只有当依赖的 m 变化后才会从新计算函数
      

useCallback

  • 作用:

    useMemo的语法糖,用于优化缓存一个函数时的代码书写。

  • 使用:

    使用useMemo缓存一个函数时,我们需要通过函数来返回函数,而useCallback优化了这个写法:

    // useMemo 通过函数返回函数
    const myFn = useMemo( ()=> (x)=>log(x) , [m])
    
    // useCallback 直接传入需要缓存的函数
    const myFn = useCallback( (x)=>log(x) , [m] )
    

useRef

  • 作用:

    useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。 ref可以保存任何可变值,甚至可以用于保存DOM节点。

  • 使用:

    代码示例:const refContainer = useRef(initialValue);

    常用方式命令式地访问子组件

    如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点,这是常见的访问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>
        </>
      );
    }
    
  • 使用细节:

    1. useRef 保存的数据的变化不会触发React组件的重新渲染。但是,我们可以借助setState来实现监听ref从而实现更新ref自动渲染,即当ref.current改变时调用setState()
    2. useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象

forwardRef

  • 作用:

    用于传递props无法传递的属性ref。

  • 使用:

    通过React.forwardRef(props,ref)来声明组件:

    function App() {
      const buttonRef = useRef(null);
      const onclick = ()=>{
        console.log(buttonRef.current)
      }
      return (
        <div className="App">
          <!-- 这里的“按钮”,相当于value通过props传递过去了 -->
          <Button3 ref={buttonRef}>按钮</Button3>
          <button onClick={onclick}>log buttonRef</button>
        </div>
      );
    }
    // 这里不是使用 function Button3(props){} 声明组件
    const Button3 = React.forwardRef((props, ref) => {
      // {...props}是JSX语法
      return (
       	<>
          	<div>button3</div>
          	<button/>
          	<button style={{ color: "red" }} ref={ref} {...props} />
        </>
      );
    });
    

    上面的实例中,我们将创建的ref(buttonRef)传递到了<button className="red" ref={ref} {...props} />上。当点击父组件的 log buttonRef 按钮时,会log出 <button style="color: red;">按钮</button>,说明我们已经获取到了这个<button>的引用。

useImperativeHandle

  • 作用:

    useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值useImperativeHandle 应当与 forwardRef 一起使用。

  • 使用:

    function App() {
      const buttonRef = useRef(null);
      useEffect(() => {
        console.log(buttonRef.current);
      });
      return (
        <div className="App">
          <Button2 ref={buttonRef}>按钮</Button2>
          <button
            className="close"
            onClick={() => {
              console.log(buttonRef);
              buttonRef.current.x();  
            }}
          >
            x
          </button>
        </div>
      );
    }
    
    // 使用了useImperativeHandle
    const Button2 = React.forwardRef((props, ref) => {
      const realButton = useRef(null);
      // 自定义暴露给父组件的实例值
      useImperativeHandle(ref, () => {
        return {
          x: () => {
            realButton.current.remove();
          },
          realButton: realButton
        };
      });
      return <button ref={realButton} {...props} />;
    });
    
    // 不使用useImperativeHandle
    const Button2 = React.forwardRef((props, ref) => {
      return <button ref={ref} {...props} />;
    });
    

    上面的2种情况,我们通过父组件logbuttonRef.current

    1. 使用了useImperativeHandle的结果:Object {x: function x(), realButton: Object}
    2. 没有使用的结果则为:<button>按钮</button>
    3. 我们甚至可以通过buttonRef.current.x()调用子组件暴露给父组件的方法。

自定义hooks

这是React Hooks最棒的地方,能够自由组合实现契合项目需求的hooks。

下面是一个自定义hooks实例:

使用这个自定义hook能够在组件渲染时获取list数据,以及处理list数据的2个方法。

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); 
  return {
    list: list,
    addItem: name => {
      setList([...list, { id: Math.random(), name: name }]);
    },
    deleteIndex: index => {
      setList(list.slice(0, index).concat(list.slice(index + 1)));
    }
  };
};
export default useList;