最新整理 React Hooks 系列(一)

242 阅读21分钟

useState 介绍

useState 用于函数式组件中声明状态变量,类似于类组件中的 state 对象。同时使用 useState 声明的状态变量可以添加到组件中,同时状态变量也是响应式的。

语法

initialState 是初始状态,state 是当前状态,setState 是更新函数。

const [state, setState] = useState(initialState)

用法

1.向组件中添加状态,并根据之前的状态改变状态。

在 set 函数中有两种方式更新状态。

第一种:直接传递下一个状态。

const [age, setAge] = useState(10)
const handleClick = () => {
  setAge(11)
}
// 或者
// const handleClick = () => {
//  setAge(age + 1)
// }
return <button onClick={handleClick}>改变状态{age}</button>

注意事项:在 set 函数被调用后,并不能立即获取新的状态值,这是由于 set 函数仅更新下一次渲染的状态变量。

const [age, setAge] = useState(10)

const handleClick = () => {
  setAge(age + 1)
  console.log(age) // 10
}

如果你想获取更新后的状态,可以添加 useEffect 副作用函数。

const [age, setAge] = useState(10)

useEffect(() => {
  console.log(age) // 11
}, [age])

const handleClick = () => {
  setAge(age + 1)
  console.log(age) // 10
}

第二种:传递更新函数。

const [age, setAge] = useState(10)

const handleClick = () => {
  setAge(val => val + 1) //  setAge(10 => 11)
}

区别:通过上面的注意事项,我们知道 set 函数后并不会获取最新最新的状态变量,所以我们可以大胆的猜测,在一个点击事件中多次调用 set 并不会更新 set 的值。例如:

const [age, setAge] = useState(10)

const handleClick = () => {
  setAge(age + 1) // setAge(10 + 1)
  console.log(age) // 10
  setAge(age + 1) // setAge(10 + 1)
  setAge(age + 1) // setAge(10 + 1)
}

这是由于 set 函数的执行是异步的,而每次 set 时,当前的状态仍然是并没有改变。如果我们希望下一次的 set 会根据上一次 set 的结果进行 set,那么不妨尝试使用更新函数的方式。

const AddState = () => {
  const [age, setAge] = useState(10)

  const handleClick = () => {
    // 直接变更状态
    // setAge(age + 1)
    // setAge(age + 1)
    // setAge(age + 1)

    // 传递一个更新函数
    setAge(val => val + 1)
    setAge(val => val + 1)
    setAge(val => val + 1)
  }
  return (
    <div className="box">
      <p>1、向组件中添加状态,并根据之前的状态改变状态</p>
      <button onClick={handleClick}>age: {age} </button>
    </div>
  )
}

为什么传递一个更新函数,会根据上一次的 set 结果去计算下一次的 set 值呢?

答:react 会将传递的更新函数放入一个队列中,并在下一次渲染期间依次调用它们,当依次调用的时候会将上一次更新函数的状态挂起,从而计算出下一个状态,并返回下一个状态值。当队列中的更新函数更新完成后,react 会将最后一次更新函数返回的结果作为当前的状态。

2. 更新状态中的对象和数组

解释:在 react 中状态允许是对象的形式,同时状态又是只读的,所以对于状态的变更,必须通过 set 函数进行,如果你直接变更状态,react 并不会监听到变化,这样视图就不会更新。所以当状态是一个对象或者数组的时候,调用 set 函数去替换原来的状态,而不是手动的修改状态。

const UpdateObject = () => {
  const [person, setPerson] = useState({
    name: 'zhuangshan',
    age: 20
  })
  const handleClick = () => {
    // 🙅错误做法
    // person.name = 'lishi'

    // 🙆正确做法
    setPerson({
      ...person,
      name: 'lishi'
    })
  }
  return (
    <div className="box">
      <p>2、更新状态中的对象和数组</p>
      <button onClick={handleClick}>
        name: {person.name} == age:{person.age}
      </button>
    </div>
  )
}

3. 如何避免重新创建初始化状态

当我们初始化状态是昂贵的计算时,可以通过传递一个初始化函数去避免每次更新状态都去执行昂贵的计算。

例如初始化的时候需要渲染一个 todolist。

const NotRepeatCreate = () => {
  function createInitialTodos() {
    const initialTodos = []
    for (let i = 0; i < 50; i++) {
      initialTodos.push({
        id: i,
        text: 'Item ' + (i + 1)
      })
    }
    console.log('initialTodos', initialTodos)
    return initialTodos
  }

  // 🙅错误做法
  // const [todos, setTodos] = useState(createInitialTodos())
  const [todos, setTodos] = useState(createInitialTodos)
  const [text, setText] = useState('')

  const addClick = () => {
    setText('')
    setTodos([
      {
        id: todos.length,
        text: text
      },
      ...todos
    ])
  }
  const onChange = e => {
    setText(e.target.value)
  }
  return (
    <div className="box">
      <p>3、如何避免重新创建初始化状态</p>
      <input value={text} onChange={onChange} />
      <button onClick={addClick}>Add</button>
      <ul className="list">
        {todos.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  )
}

代码演示

上面初始化状态时拿到的 createInitialTodos 函数的结果,所以当每一次 todos 的状态改变的时候,createInitialTodos 函数都会重新计算。为了避免这种计算,可以使用初始化函数的方式,让 react 去缓存你的初始状态。

4. 通过键值重置组件状态

在 react 中当父组件向子组件中传递一个参数的时候,这个参数如果改变,那么子组件就会重新渲染,所以下面的 Input 组件中传递的 key 值改变的时候,Input 输入框中的状态就会变成初始状态,这是由于 react 组件自上而下的更新机制决定的。

const KeyResetComponent = () => {
  function Input() {
    const [value, setValue] = useState('zhaungshan')
    return <input onChange={e => setValue(e.target.value)} value={value} />
  }
  const [key, setKey] = useState(0)
  const handleReset = () => {
    setKey(key + 1)
  }
  return (
    <div className="box">
      <p>4、通过键值重置组件状态</p>
      <button onClick={handleReset}>Reset</button>
      <Input key={key} />
    </div>
  )
}

代码演示

故障排查

1.状态改变,页面没有更新

例如:当初始状态是一个对象或者数组的时候,尝试在 set 之前对当前状态进行更改,这种做法是无效的,因为 react 会对下一个状态和当一个状态进行比对,采用的 Object.is 的方式,当比对的结果为 true 时 react 会忽略你的更新。

const Malfunction1 = () => {
  const [person, SetPerson] = useState({ name: 'zhaungshan', age: 18 })
  const handleChange = () => {
    // 🙅错误示范
    person.age = 19
    console.log('person', person) // {name: 'zhaungshan', age: 19}
    // 在set之前会进行 Object.is 比对,所以在这里比对的是  Object.is(person,person) 无论对person对象如何修改返回的都true。
    SetPerson(person)
  }

  return (
    <div className="box">
      <button onClick={handleChange}>age:{person.age}</button>
    </div>
  )
}

同理,在第二步的更新状态是数组和对象也提到了,状态是只读的,所以对于状态的更新是替换,而不是修改。正确做法如下:

const Malfunction1 = () => {
  const [person, SetPerson] = useState({ name: 'zhaungshan', age: 18 })
  const handleChange = () => {
    // 🙆正确做法
    SetPerson({ ...person, age: 19 })
  }
  return (
    <div className="box">
      <button onClick={handleChange}>age:{person.age}</button>
    </div>
  )
}

2.状态改变,日志记录没有更新?

在之前的 set 用法的介绍中已经说过了,更新 set 函数后并不能立即获取更新后的值,react 会在下一次渲染时获取更新后的值。

const Malfunction2 = () => {
  const [person, SetPerson] = useState({ name: 'zhaungshan', age: 18 })

  const handleChange = () => {
    // 🙅错误示范
    SetPerson({ ...person, age: person.age + 1 })
    console.log(person.age) // 18
  }

  return (
    <div className="box">
      <p>6、故障排查:状态改变,日志记录没有更新?</p>
      <button onClick={handleChange}>age:{person.age}</button>
    </div>
  )
}

正确的做法是将需要 set 的值保存在变量中。

const Malfunction2 = () => {
  const [person, SetPerson] = useState({ name: 'zhaungshan', age: 18 })

  const handleChange = () => {
    // 🙆正确做法
    const Nage = person.age + 1
    SetPerson({ ...person, age: Nage })
  }

  return (
    <div className="box">
      <button onClick={handleChange}>age:{person.age}</button>
    </div>
  )
}

useEffect 介绍

useEffect 是一个重要的 hook ,可以实现函数组件中执行各种副作用操作,(比如:发送网络请求,手动变更 DOM,记录日志等)

语法

useEffect(setup, dependencies?)

setup 是一个执行函数。dependencies 是依赖项,是可选的。当不传 dependencies 依赖项时,组件每次渲染都会导致 setup 函数的重新执行。当 dependencies 是一个空数组的时候,在组件渲染完成后会执行 setup 函数。当 dependencies 中存在依赖项时,组件第一次渲染会和依赖项发生改变都会执行 setup 函数。

用法

1.useEffect 分别模拟了类组件中的哪些生命周期?

1.1 将 useEffect 的依赖项设置成一个空数组,这个时候 useEffect 相当于类组件中的 componentDidMount ,在组件挂载之后执行。

useEffect(() => {
  // 相当于 componentDidMount 中的代码
  console.log('组件挂载后执行')
}, [])

1.2 将 useEffect 的依赖项设置成监听的变量,这个时候 useEffect 相当于类组件中的 componentDidUpdate ,在组件的依赖值发生改变时执行。

const Simulation = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 相当于 componentDidUpdate 中的代码,当 count 变化时执行
    console.log(`count 更新为: ${count}`)
  }, [count])

  return (
    <div className="box">
      <button onChange={() => setCount(a => a + 1)}>计数器:{count}</button>
    </div>
  )
}

1.3 将 useEffect 中返回一个清理函数,相当于类组件中的 componentWillUnmount,组件卸载前执行。

useEffect(() => {
  // 组件挂载或更新时执行的代码
  const timer = setInterval(() => {
    console.log('每秒执行一次')
  }, 1000)

  // 返回一个清理函数,组件卸载前执行
  return () => {
    clearInterval(timer)
    console.log('组件卸载,清除定时器')
  }
}, [])

2.连接外部系统,控制模态对话框

接下来这个案例是关于使用 useEffect 连接外部系统,控制模态对话框的操作,先看代码。

const ConnectionExternal = () => {
  const ModalDialog = ({ isOpen, children }) => {
    const ref = useRef()
    useEffect(() => {
      const dialog = ref.current
      if (!isOpen) {
        return
      }
      dialog.showModal()
      return () => {
        dialog.close()
      }
    }, [isOpen])
    return <dialog ref={ref}>{children}</dialog>
  }

  const [show, setShow] = useState(false)

  const openDialog = () => {
    setShow(true)
  }

  const onClose = () => {
    setShow(false)
  }

  return (
    <div className="box">
      <p>2.连接外部系统,控制模态对话框</p>
      <button onClick={openDialog}>open dialog</button>
      <ModalDialog isOpen={show}>
        Hello!
        <br />
        <button onClick={onClose}>Close</button>
      </ModalDialog>
    </div>
  )
}

代码演示

上面的案例中是通过父组件中一个 show 状态去控制子组件中模块框的显示和隐藏。ModalDialog 组件通过 useEffect 的副作用让 isOpen 属性控制到 showModal 方法和 close 方法的调用。

3.根据副作用的先前状态更新状态

一个常见的场景,在页面上设置一个计数的功能,每隔一秒钟让计数器 count 加 1,当然可以当 count 作为 useEffect 的依赖项,这也会导致每一次的 count 变化,useEffect 都会执行一次清理副作用和设置副作用的操作,例如:

const BeforeStateUpdate = () => {
  const [count, setCount] = useState(0)
  // 第一种使用count作为副作用的依赖值
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    return () => {
      console.log('intervalId', intervalId)
      clearInterval(intervalId)
    }
  }, [count])

  return (
    <div className="box">
      <div>状态:{count}</div>
    </div>
  )
}

对于上面的 useEffect 的频繁操作并不是必要的,可以利用 set 函数的中传递一个更新函数去解决。

const BeforeStateUpdate = () => {
  const [count, setCount] = useState(0)

  // 第二种不使用count作为副作用的依赖值
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(a => a + 1)
    }, 1000)
    return () => {
      console.log('intervalId', intervalId)
      clearInterval(intervalId)
    }
  }, [])

  return (
    <div className="box">
      <div>状态:{count}</div>
    </div>
  )
}

4.避免将对象或者函数作为 useEffect 的依赖项。

在 useEffect 的依赖项选择中,尽量避免使用对象或者函数作为依赖,当使用函数或者对象作为依赖项并在执行函数中去修改它,这样很有可能就会造成一种死循环,如下面这种。

const NeedlessObjectRely = () => {
  const [person, setPerson] = useState({
    name: 'zangsan',
    sex: 'man'
  })

  useEffect(() => {
    // 🙅错误示范: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
    setPerson({
      ...person,
      name: 'lisi'
    })
  }, [person])
  return (
    <div className="box">
      <p> </p>
      <div>姓名:{person.name}</div>
    </div>
  )
}

如果你必要在 useEffect 中去修改对象的属性,可以使用 set 函数时使用更新函数的形式。

const NeedlessObjectRely = () => {
  const [person, setPerson] = useState({
    name: 'zangsan',
    sex: 'man'
  })

  // 🙆正确做法
  useEffect(() => {
    setPerson(p => {
      return {
        ...p,
        name: 'lisi'
      }
    })
  }, [])
  return (
    <div className="box">
      <p> </p>
      <div>姓名:{person.name}</div>
    </div>
  )
}

useLayoutEffect 介绍

useLayoutEffect 和 useEffect 都是处理副作用的 hook,区别是两者的执行时机不同。

语法

setup:setup 是 useLayoutEffect 的设置函数,设置函数会在组件被添加到 DOM 之前执行,同时在设置函数中可以返回一个清理函数,清理函数执行时机是在每次依赖(dependencies)发生变化重新渲染后,react 会使用旧值运行清理函数,然后用新值运行你的设置函数,所以组件在从 DOM 删除之前,react 会设置你的清理函数。这也是定时器清理常见用法。

dependencies:dependencies 是 setup 执行的依赖列表,dependencies 是可选的,用法和 useEffect 中 dependencies 的用法一直。

useLayoutEffect(setup, dependencies?)

用法

1. useLayoutEffect 中设置函数和清理函数的区别?

上面对于 setup 的介绍中已经阐述了设置函数和清理函数的区别,这里主要是通过代码验证上面的说法。

const BasicUse = () => {
  const [num, setNum] = useState(1)

  useLayoutEffect(() => {
    console.log('设置函数', num)
    return () => {
      console.log('清理函数', num)
    }
  }, [num])

  const onChange = () => {
    setNum(a => a + 1)
  }

  return (
    <div className="box">
      <p>1.useLayoutEffect中设置函数和清理函数的区别</p>
      <button onClick={onChange}>点击加1</button>
    </div>
  )
}

代码演示

上面的代码中,当初始化渲染的时候只执行了设置函数,同时打印了 1,当每次点击加 1 的时候,清理函数的执行时机会在设置函数之前,同时清理函数中打印的依赖值是依赖值变化之前的值,而设置函数中打印的依赖值是依赖变化后的值。

2. useLayoutEffect 和 useEffect 用法对比?

先看一个案例:

const ElementPosition = () => {
  const [isBig, setIsBig] = useState(false)
  const handleClick = () => {
    setIsBig(false)
  }
  // 人为减缓渲染速度
  let now = performance.now()
  while (performance.now() - now < 400) {}

  useEffect(() => {
    console.log('useEffect', isBig)
    setIsBig(true)
  }, [isBig])

  return (
    <div className="box">
      <p>1.useLayoutEffect和useEffect用法对比</p>
      <div className={isBig ? 'big' : 'small'} />
      <button onClick={handleClick}>变小</button>
    </div>
  )
}

代码演示

上面的代码中 isBig 变量初始化渲染的时候是 false,所以一开始的时候 div 是小的,当组件渲染完成后在 useEffect 中又改变了 isBig 的状态,这个时候 isBig 变成了 ture,由于 isBig 状态的改变导致 div 会被重新渲染,这里人为的减缓了渲染的速度,所以可以看到页面中的 div 是先变小后变大。当然你点击按钮手动的改变 isBig 的状态也是可以看到 div 是先变小后变大的过程。

如果上面的代码变成 useLayoutEffect 会怎么样呢?

const ElementPosition = () => {
  const [isBig, setIsBig] = useState(false)
  const handleClick = () => {
    setIsBig(false)
  }

  // 人为减缓渲染速度
  let now = performance.now()
  while (performance.now() - now < 200) {}

  useLayoutEffect(() => {
    console.log('useLayoutEffect', isBig)
    setIsBig(true)
  }, [isBig])

  return (
    <div className="box">
      <p>2.useLayoutEffect和useEffect用法对比</p>
      <div className={isBig ? 'big' : 'small'} />
      <button onClick={handleClick}>变小</button>
    </div>
  )
}

代码演示

相同的操作,只是将 useEffect 变成 useLayoutEffect。神奇的一幕出现了,那就是无论是初始化渲染,还是点击按钮,页面中的 div 一直是大的,并没有变小,这是为什么呢?

从 useLayoutEffect 的官方介绍中就可以看出,useLayoutEffect 的执行时机是屏幕绘制之前执行,所以在 useLayoutEffect 中更新状态会阻止浏览器重新绘制屏幕。结合上面的案例,当初始化渲染的时候 isBig 是 false,当浏览器还没有开始绘制 div 的时候,突然发现在 useLayoutEffect 中又重新设置了 isBig 的状态,所以浏览器会等待 isBig 的状态改变之后同步进行绘制,这导致了我们并不能在页面上看到 div 样式的改变。

总结

1. useEffect 和 useLayoutEffect 的区别?

1.1 执行时机不同:useEffect 的执行时机是在组件首次渲染和更新渲染之后异步执行的,这就意味着 useEffect 的执行并不会阻塞组件的渲染,也不会影响到用户的交互体验。相比之下,useLayoutEffect 的执行是在组件完成渲染之后,浏览器绘制之前同步执行的。这就意味着在 DOM 完成渲染之后,浏览器绘制之前执行的因此会阻塞浏览器的渲染。

1.2 执行的时间点不同:useEffect 的执行是在组件渲染完成后的‘提交阶段’异步执行的,这就导致它并不会阻塞浏览器的绘制。同时这种异步的特性使得它在处理如数据获取、订阅事件等需要等待的副作用操作时非常有用。useLayoutEffect 的执行时间点是在组件的渲染完成后的‘布局阶段’执行的,同时在浏览器绘制屏幕之前同步执行的。所以它的副作用并不会引起渲染跳跃,可以提供更流畅的用户体验,如果在 useLayoutEffect 中操作非常耗时时,导致页面响应过慢,影响用户操作体验。

1.3 应用场景不同:大多数的场景都是使用 useEffect,比如异步获取数据,组件卸载前清理订阅事件。useLayoutEffect 可以用于 DOM 更新后立即获取元素的尺寸和位置。

useRef 介绍

useRef 允许你在组件的整个生命周期内保持一个不需要渲染的值的引用,useRef 返回的是一个可变的 ref 对象。

语法

const ref = useRef(initialValue)

用法

1. useRef 存储的变量和常规变量的区别?

const VariateComparison = () => {
  let [num, setNum] = useState(1)
  let ref = useRef(null)
  let variate = null

  const handleChange = () => {
    setNum(a => a + 1)
  }

  useEffect(() => {
    ref.current = num
    // 对于variate的赋值会在每次渲染前丢失
    // variate = num
  }, [num])

  if (num !== 1) {
    console.log('ref.current', ref.current) // 获取的是每次num更新前的值
    console.log('variate', variate) // 获取的始终是全局状态null
  }

  return (
    <div className="box">
      <p>1.useRef存储的变量和常规变量的区别</p>
      <button onClick={handleChange}>更新数字:{num}</button>
    </div>
  )
}

代码演示

useRef 允许你在每次渲染之间存储信息,而常规的变量会在渲染时被重置。所以当点击更新数字的时候,虽然在 num 每次更新的时候都会把 num 的值分别赋值给 ref.current 和变量 variate,但是在重新渲染的时候 ref.current 的值被保存而 variate 的值被重置。

2. 更新 useRef 不会触发重新渲染。

const UpdateRef = () => {
  const num = useRef(1)
  const handleChange = () => {
    num.current = 2
  }

  return (
    <div className="box">
      <p>2.更改useRef不会触发重新渲染</p>
      <button onClick={handleChange}>更新数字:{num.current}</button>
    </div>
  )
}

代码演示

与 useState 不同,更新 useState 会触发重新渲染,而更新 useRef 不会触发重新渲染。

3. 操作 DOM,聚焦文本输入。

使用引用操作 DOM 是一种常见的做法,首先使用 useRef 创建一个引用对象,然后将引用对象作为 ref 传递给你需要操作的 DOM,然后通过引用对象的 current 属性拿到该 DOM 的节点。

const FocusingInput = () => {
  const inputRef = useRef(null)

  const handleClick = () => {
    inputRef.current.focus()
  }
  return (
    <div className="box">
      <p>3.使用useRef操作DOM,聚焦文本输入</p>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus the input</button>
    </div>
  )
}

代码演示

上面的案例就是通过获取 input 的 DOM 使文本聚焦。

故障排查

4.无法获取自定义组件的引用?

当我们尝试获取自定义租价的 ref 时,这时控制台会提示函数组件不能给出 refs,但是可以用 React.forwardRef()。

const CustomComponent = () => {
  const inputRef = useRef(null)

  const handleClick = () => {
    console.log(inputRef.current)
  }
  const MyInput = () => {
    return <input />
  }
  return (
    <div className="box">
      <p>4.无法获取自定义组件的引用</p>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>获取DOM</button>
    </div>
  )
}

使用 forwardRef 去包裹一下 ref,并将 ref 传递给自组件的 ref,这个时候就可以获取到子组件的 ref 了。

const CustomComponent = () => {
  const inputRef = useRef(null)

  const handleClick = () => {
    console.log(inputRef.current)
  }

  const MyInput = forwardRef((props, ref) => {
    return <input ref={ref} />
  })

  return (
    <div className="box">
      <p>4.使用forwardRef获取自定义组件的引用</p>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>获取DOM</button>
    </div>
  )
}

代码演示

useMemo 介绍

官方定义:useMemo 是一个 React 钩子,可让你在重新渲染之间缓存计算结果。 个人理解:useMemo 用于缓存函数的结果,需要 return 一个值,学过 vue 中 computed 应该会比较容易理解。

语法

useMemo 的使用有点像 vue 中的计算属性,calculateValue 是用于计算要缓存的值的函数,并返回计算后的结果,dependencies 是依赖值,是一个数组,当其中任何一项依赖发生改变的时候都会重新计算。react 使用的 Object.is 对依赖的值进行比较。

const cachedValue = useMemo(calculateValue, dependencies)

用法

1. 跳过昂贵的重新计算

案例如下:当父组件中切换主题的时候和切换选项的时候都会造成 visibleTodos 函数重新计算,显然我们想要的是当切换选项的时候 visibleTodos 重新计算,而切换祖主题的时候并不需要重新去计算 visibleTodos。

function filterTodos(todos, tab) {
  if (tab === 'completed') {
    return todos.filter(item => item.completed)
  } else {
    return todos
  }
}
const TodoList = ({ tab, todos, theme }) => {
  const visibleTodos = () => {
    console.log('filterTodo重新计算了')
    return filterTodos(todos, tab)
  }

  return (
    <div className={theme}>
      <ul>
        {visibleTodos().map(todo => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
    </div>
  )
}

function createTodos() {
  const todos = []
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: 'Todo ' + (i + 1),
      completed: Math.random() > 0.9
    })
  }
  return todos
}

const todos = createTodos()

const ExpensiveComponent = () => {
  const [tab, setTab] = useState('completed')
  const [isDark, setIsDark] = useState(false)
  const darkChange = e => {
    setIsDark(e.target.checked)
  }

  return (
    <div className="box">
      <p>1.跳过昂贵的重新计算。</p>
      <label>
        <input type="checkbox" checked={isDark} onChange={darkChange} />
        Dark mode
      </label>
      <br />
      <button onClick={() => setTab('all')}>All</button>
      <button onClick={() => setTab('completed')}>completed</button>
      <TodoList tab={tab} todos={todos} theme={isDark ? 'dark' : 'light'} />
    </div>
  )
}

代码演示

对于上面出现的问题,可以使用 useMemo 对 visibleTodos 方法进行包裹,这样当依赖的 todos 和 tab 发生变化的时候,visibleTodos 才会进行重新计算,而切换主题并不会造成 visibleTodos 的计算。

function filterTodos(todos, tab) {
  if (tab === 'completed') {
    return todos.filter(item => item.completed)
  } else {
    return todos
  }
}
const TodoList = ({ tab, todos, theme }) => {
  const visibleTodos = useMemo(() => {
    console.log('filterTodo重新计算了')
    return filterTodos(todos, tab)
  }, [todos, tab])

  return (
    <div className={theme}>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
    </div>
  )
}

function createTodos() {
  const todos = []
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: 'Todo ' + (i + 1),
      completed: Math.random() > 0.9
    })
  }
  return todos
}

const todos = createTodos()

const ExpensiveComponent = () => {
  const [tab, setTab] = useState('completed')
  const [isDark, setIsDark] = useState(false)
  const darkChange = e => {
    setIsDark(e.target.checked)
  }

  return (
    <div className="box">
      <p>1.跳过昂贵的重新计算。</p>
      <label>
        <input type="checkbox" checked={isDark} onChange={darkChange} />
        Dark mode
      </label>
      <br />
      <button onClick={() => setTab('all')}>All</button>
      <button onClick={() => setTab('completed')}>completed</button>
      <TodoList tab={tab} todos={todos} theme={isDark ? 'dark' : 'light'} />
    </div>
  )
}

代码演示

2. 跳过组件的重新渲染

在 react 中当一个组件重新渲染时,react 会递归的依次渲染它的子集。如下面这种当父组件切换夜间模式的时候,Child 组件会重新渲染。

const Child = () => {
  console.log('Child重新渲染了')
  return <div>Child</div>
}

const RepeatRendering = () => {
  const [isDark, setIsDark] = useState(false)
  const darkChange = e => {
    setIsDark(e.target.checked)
  }
  return (
    <div className="box">
      <p>2.父组件的渲染会递归渲染它的子集</p>
      <div className={isDark ? 'dark' : 'light'}>
        <label>
          <input type="checkbox" checked={isDark} onChange={darkChange} />
          Dark mode
        </label>
        <Child theme={isDark ? 'dark' : 'light'} />
      </div>
    </div>
  )
}

代码演示

当然,这是我们不希望的,切换主题是不需要重新渲染子组件的。

对此,我们可以使用 memo 对不需要重新渲染的子组件进行包裹,memo 的作用是可以在属性不变的情况下跳过重新渲染组件。

const Child = memo(() => {
  console.log('Child重新渲染了')
  return <div>Child</div>
})
const RepeatRendering = () => {
  const [isDark, setIsDark] = useState(false)
  const darkChange = e => {
    setIsDark(e.target.checked)
  }
  return (
    <div className="box">
      <p>3.父组件渲染时跳过子集的渲染</p>
      <div className={isDark ? 'dark' : 'light'}>
        <label>
          <input type="checkbox" checked={isDark} onChange={darkChange} />
          Dark mode
        </label>
        <Child />
      </div>
    </div>
  )
}

代码演示

上面的案例中,使用 memo 包裹的子组件在切换主题的时候不会重新渲染,但是有时候子组件需要接受父组件的参数用于展示,比如下面这种在父组件中传递给子组件一个对象。

const Child = memo(({ person }) => {
  console.log('Child重新渲染了', person)
  return (
    <div>
      <div>姓名:{person.name}</div>
      <div>年龄:{person.age}</div>
    </div>
  )
})
const RepeatRendering = () => {
  const [isDark, setIsDark] = useState(false)
  const [person, setPerson] = useState({
    name: 'Jack',
    age: 10
  })
  const darkChange = e => {
    setIsDark(e.target.checked)
  }

  const addPersonAge = () => {
    setPerson(a => {
      return {
        age: a.age + 1,
        name: a.name
      }
    })
  }

  const per = {
    name: 'chen' + person.name,
    age: person.age
  }

  return (
    <div className="box">
      <p>3.父组件渲染时无法避免子组件的渲染</p>
      <div className={isDark ? 'dark' : 'light'}>
        <label>
          <input type="checkbox" checked={isDark} onChange={darkChange} />
          Dark mode
        </label>
        <br />
        <button onClick={addPersonAge}>增加组件年龄</button>
        <Child person={per} />
      </div>
    </div>
  )
}

代码演示

可以看出,当父组件传递一个 person 对象给子组件的时候,虽然每次 person 中属性的变化都会去渲染子组件,这是我们想要的,但是每次切换主题的时候子组件也会重新渲染,显然,这并不是我们想要的,这是由于 memo 对于传入的属性采用的是 Object.is 进行比对的,所以每次切换主题的时候生成的 person 对象和上一次的都会不同,所以会造成重新渲染子组件。

所以这个时候单纯的依赖 memo 就不起作用了,需要结合 useMemo 一起使用。

const Child = memo(({ person }) => {
  console.log('Child重新渲染了', person)
  return (
    <div>
      <div>姓名:{person.name}</div>
      <div>年龄:{person.age}</div>
    </div>
  )
})
const RepeatRendering = () => {
  const [isDark, setIsDark] = useState(false)
  const [person, setPerson] = useState({
    name: 'Jack',
    age: 10
  })
  const darkChange = e => {
    setIsDark(e.target.checked)
  }

  const addPersonAge = () => {
    setPerson(a => {
      return {
        age: a.age + 1,
        name: a.name
      }
    })
  }

  const per = useMemo(() => {
    return {
      name: 'chen' + person.name,
      age: person.age
    }
  }, [person.age, person.name])

  return (
    <div className="box">
      <p>4.父组件渲染时有选择的渲染子集</p>
      <div className={isDark ? 'dark' : 'light'}>
        <label>
          <input type="checkbox" checked={isDark} onChange={darkChange} />
          Dark mode
        </label>
        <br />
        <button onClick={addPersonAge}>增加组件年龄</button>
        <Child person={per} />
      </div>
    </div>
  )
}

export default UseMemo

代码演示

上面就是使用 useMemo 对依赖的 person 属性进行包裹,当依赖的 name 和 age 发生变化的就会返回一个新的对象,否则就缓存之前的对象。

useCallback 介绍

官方定义:useCallback 是一个 React 钩子,可让你在重新渲染之间缓存函数定义。

个人理解:useCallback 在组件渲染期间用于缓存一个函数的引用。

语法

const cachedFn = useCallback(fn, dependencies)

参数:

  1. fn:要缓存的函数值。在下一次渲染中,如果 dependencies 没有发生改变,react 将返回相同的函数,如果 dependencies 发生改变,则返回新传递的函数。

  2. dependencies:fn 代码中引用的所有反应值的列表。也可以理解是 fn 函数的依赖值。react 将使用 Object.is 对当前的依赖和上一次的依赖进行比对。

用法

1.跳过组件的重新渲染

案例:当父组件传递给子组件一个函数的时候,在父组件的中每次更改主题,都会导致子组件重新渲染。如下面的代码向子组件中传递一个 handleSubmit 函数, 在子组件中 Submit 中,即使是每次父组件主题的切换仍然会导致子组件的重新渲染,显然这种是不需要的。

const Submit = memo(({ onSubmit }) => {
  let params = { ...onSubmit() }
  console.log('当输入框变化时才渲染页面', params)
  const submitRequest = () => {
    console.log(params)
  }
  return <button onClick={submitRequest}>提交</button>
})

const RepeatRendering = () => {
  const [email, setEmail] = useState('')
  const [name, setName] = useState('')
  const [isDark, setIsDark] = useState(false)

  const handleEmail = e => {
    setEmail(e.target.value)
  }
  const handleName = e => {
    setName(e.target.value)
  }

  const changeDark = e => {
    setIsDark(e.target.checked)
  }

  // 不使用 useCallback
  const handleSubmit = () => {
    return {
      email: email,
      name: name
    }
  }

  return (
    <div className="box" style={{ background: isDark ? '#000' : '#eaeaea' }}>
      <label>
        <input type="checkbox" checked={isDark} onChange={changeDark} />
        Dark mode
      </label>
      <br />
      <div>
        <label>姓名:</label>
        <input type="text" id="name" value={name} onChange={handleName} />
        <br />
        <label>邮箱:</label>
        <input type="email" id="email" value={email} onChange={handleEmail} />
        <br />
        <Submit onSubmit={handleSubmit} />
      </div>
    </div>
  )
}

代码演示

当切换主题的时候不需要子组件的渲染,这个时候可以在传递的函数 handleSubmit 的时候使用 useCallback 包裹一下,并且设置依赖的值是 email 和 name,只有当依赖的值发生变化的时候才去渲染子组件。

const Submit = memo(({ onSubmit }) => {
  let params = { ...onSubmit() }
  console.log('当输入框变化时才渲染页面', params)
  const submitRequest = () => {
    console.log(params)
  }
  return <button onClick={submitRequest}>提交</button>
})

const RepeatRendering = () => {
  const [email, setEmail] = useState('')
  const [name, setName] = useState('')
  const [isDark, setIsDark] = useState(false)

  const handleEmail = e => {
    setEmail(e.target.value)
  }
  const handleName = e => {
    setName(e.target.value)
  }

  const changeDark = e => {
    setIsDark(e.target.checked)
  }

  // 使用 useCallback
  const handleSubmit = useCallback(() => {
    return {
      email: email,
      name: name
    }
  }, [name, email])

  return (
    <div className="box" style={{ background: isDark ? '#000' : '#eaeaea' }}>
      <label>
        <input type="checkbox" checked={isDark} onChange={changeDark} />
        Dark mode
      </label>
      <br />
      <div>
        <label>姓名:</label>
        <input type="text" id="name" value={name} onChange={handleName} />
        <br />
        <label>邮箱:</label>
        <input type="email" id="email" value={email} onChange={handleEmail} />
        <br />
        <Submit onSubmit={handleSubmit} />
      </div>
    </div>
  )
}

代码演示

总结

1. useCallback 和 useMemo 的区别?

答:useCallback 用于缓存函数的自身,而 useMemo 用于函数函数的结果, useCallback 并不像 useMemo 会去调用函数,而是去缓存函数,这样便于在函数的传递过程中不会造成子组件的重复渲染。

2. 需要在什么地方添加 useCallback?

答:大多数的场景是不需要记忆化,如果你的项目是像绘图编辑器一样,并且大多数交互都是颗粒状的(如移动形状),这个时候 useCallback 是非常有用的。如果你想将函数作为属性传递给封装的 memo 时,可以考虑使用 usecallback 跳过那些重新渲染。