React Hooks入门学习

80 阅读7分钟

一、什么是Hooks?

  • React一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有。
  • HooksReact 16.8新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
  • 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以直接在现有的函数组件中使用 Hooks。
  • 凡是 use 开头的 React API 都是 Hooks

二、Hooks 解决的问题

  1. 类组件的不足
      • 状态逻辑难以复用:在组件之间复用状态逻辑很难也很复杂,可能要用到render props(渲染属性)或者是 HOC(高阶组件),但这两种都需要在最外层包裹一层父容器,导致层级冗余。
      • 趋向复杂难以维护
      • 在生命周期中混杂一些可能不相干的逻辑,如在componentDidMount中注册事件或者是其他逻辑,在componentWillUnmont中卸载事件,这样分散不集中的写法,就很容易写出bug
      • 类组件中到处都是对state的访问处理,导致组件难以拆分成更小的组件。
      • this指向问题:父组件向子组件传递函数时必须绑定this

react类组件绑定this的四种方法:

前提:子组件内部做了性能优化,如React.PureComponent

  • 第一种是在构造函数中绑定 this:那么每次父组件刷新的时候,如果传递给子组件其他的 props 值不变,那么子组件就不会刷新;
  • 第二种是在 render函数里面绑定 this:因为 bind 函数会返回一个新的函数,所以每次父组件刷新时,都会重新生成一个函数,即使父组件传递给子组件其他的 props 值不变,子组件每次都会刷新;
  • 第三种是使用箭头函数:父组件刷新的时候,即使两个箭头函数的函数体是一样的,都会生成一个新的箭头函数,所以子组件每次都会刷新;
  • 第四种是使用类的静态属性:原理和第一种方法差不多,比第一种更简洁;

综上所述,如果不注意的话,很容易写成第三种写法,导致性能上有所损耗。

  1. Hooks的优势
      • 能优化类组件的三大问题;
      • 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks );
      • 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据);
      • 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行;

三、注意事项

  • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用;
  • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用;

四、React Hooks API

  • useState
import { useState } from 'react';
function Example() {
    const [count, setCount] = useState(0);
    state = { name: 'zhangsan', age: 18, };
    this.setState({ name: 'lisi' });
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}> Click me </button>
        </div>
     );
}

我们在日常开发中可以声明多个stateuseState也并不是只能接收简单numberstringboolean作为初始值,同时也可以接收复杂的object

注意:

  • 使用规则:必须放在函数组件的顶层,不允许放在循环,条件或者嵌套函数里。我们必须确保组件在每一次渲染时候都按照同样的顺序去调用Hooks;为什么?因为React是根据调用顺序将单个state和关联的useState对应起来。
  • 初始值使用问题:初始值只要在首次渲染的时候才会被使用,再次渲染的时候将会取用当前state的值进行赋值操作。所以我们在使用useState时需避免使用props的变量进行初始化。
  • setState的差异:setState是对结果进行合并,而useState是直接将值进行替换。
// ------------ // 首次渲染 // ------------
// 1. 使用 'Mary' 初始化变量名为 name 的 state
const [name, setName] = useState('Mary')
// 2. 添加 effect 以保存 form 操作
useEffect(persistForm)
// 3. 使用 'Poppins' 初始化变量名为 surname 的 state
const [surname, setSurname] = useState('Poppins')
// 4. 添加 effect 以更新标题 useEffect(updateTitle)
// ------------- // 二次渲染 // ------------- 
// 1. 读取变量名为 name 的 state(参数被忽略)
const [name, setName] useState('Mary')
// 2. 替换保存 form 的 effect
useEffect(persistForm)
// 3. 读取变量名为 surname 的 state(参数被忽略)
const [surname, setSurname] = useState('Poppins')
// 4. 替换更新标题的 effect
useEffect(updateTitle)
  • useEffect
import React, { useState, useEffect } from 'react';
function Example() { 
    const [count, setCount] = useState(0);
    // Similar to componentDidMount and componentDidUpdate:
    useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`;
        return () => {
            // do something
         }
    }, [state, props]);
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}> Click me </button>
        </div>
     );
}

注意:

  • 默认情况下,useEffect在第一次渲染之后和每次更新之后都会执行;
  • useEffect接收两个参数,第一个参数的是fn(不能是async函数),第二个参数是一个array。第二个参数是useEffect的依赖项,即当依赖发生变化时才会执行该副作用。

每个useEffect的第一个fn参数都可以有一个返回函数,除了第一次渲染外,每次执行useEffect都会先执行返回函数再执行函数体的逻辑。如果 useEffect 第一个参数传入async,返回值则变成了 Promise,会导致 react在调用销毁函数的时候报错 :function.apply is undefined

useEffect(() => {
    console.log('useEffect1')
    return () => { console.log('useEffect1 卸载了') }
});
useEffect(() => {
    console.log('useEffect2')
    return () => { console.log('useEffect2 卸载了') } 
});
useEffect(() => { 
    console.log('useEffect3')
    return () => { console.log('useEffect3 卸载了') } 
 } , []);

思考一下上面的执行顺序?

更新阶段

  1. 执行新的的useEffect 函数 , 并将effect 函数存入队列等待执行;
  2. 执行返还函数队列, 并观察返还函数书否有依赖参数, 有依赖参数, 追踪依赖参数是否改变, 改变执行, 没有改变不执行;
  3. 执行effect 函数队列, 观察effect函数是否有依赖参数,有依赖参数, 追踪依赖参数是否改变, 改变执行, 没有改变不执行;
  • useContext
const themes = {
    light: { foreground: "#000000", background: "#eeeeee" },
    dark: { foreground: "#ffffff", background: "#222222" }
};
const ThemeContext = React.createContext(themes.light);
function App() {
    return (
        <ThemeContext.Provider value={themes.dark}>
            <Toolbar />
        </ThemeContext.Provider>
    );
}
function Toolbar(props) {
    return (
        <div> <ThemedButton /> </div>
    );
}
function ThemedButton() {
    const theme = useContext(ThemeContext); 
    return (
        <button style={{ background: theme.background, color: theme.foreground }}>
            I am styled by theme context! 
        </button>
    );
}

了解useContext之前,可以先了解一下Context

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法 Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

需要注意点是:组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Providervalue时,消费组件的defaultValue不会生效。

  • useReducer
const initialState = {count: 0};
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error(); 
     }
}
function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
    );
}

第一个参数:reducer函数。第二个参数:初始化的state。返回值为最新的statedispatch函数(用来触发reducer函数,计算对应的state)

  • useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

前提知识:我们知道每次组件渲染时这个自定义handle函数时都是重新创建的一个新函数。我们将这个函数传递给子组件后,因为handle函数的引用地址变了,导致子组件使用PureComponentshouldComponentUpdateReact.memo等相关优化失效。

在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。

  • useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

ab的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的

两者差异:两者相似99%,useCallbackuseMemo都可缓存函数的引用或值,但是从更细的使用角度来说useCallback缓存函数的引用,useMemo缓存计算数据的返回值。