?前言
本文是本人在学习ReactHooks记录的学习笔记,内容不仅限于文档中的内容,涉及了Hooks源码相关。如果有错误,还请及时指正。
❓什么是Hook?为什么需要Hook?
- 逻辑复用,如果使用高阶组件等特性,较为复杂。
- 传统的函数组件无法存储state状态。
- Hooks允许在函数组件中,调用React的功能。
- 使用自定义Hook可以简化逻辑复用。
- Hooks是React的未来
- Hooks不是什么魔法。Hooks的设计也与React无关。(本文将在最后介绍Hooks的源码)
⛰️useState
useState可以在函数组件中,添加state Hook。
调用useState会返回一个state变量,以及更新state变量的方法。useState的参数是state变量的初始值,初始值仅在初次渲染时有效。
更新state变量的方法,并不会像this.setState一样,合并state。而是替换state变量。
下面是一个简单的例子, 会在页面上渲染count的值,点击setCount的按钮会更新count的值。
函数式更新
如果新的state需要之前的state计算得到。可以向useState返回的更新函数中,传递一个函数。函数的参数是前一个state。
惰性的初始值
useState的初始值是惰性的,只会在初次渲染组件的时候起作用。如果state的初始值需用通过复杂计算得到,useState的初始值也可以是一个函数,函数的返回值将是useState的初始值。
跳过state更新
调用state的更新函数,传入和当前一样的state时。React会跳过子组件的渲染,以及effect的执行。
小节
- useState的更新是替换,而不是合并。
- useState可以接收函数参数,并将函数的返回值作为初始值
- useState的更新函数可以接收函数作为参数,函数的参数是前一状态的state值。
- 使用当前的值,对state进行更新不会触发渲染。
- 可以使用多个useState,声明多个state变量。
?useEffect
除了官方文档外,推荐阅读一下Um guia completo para useEffect
useEffect可以让我们在函数组件中执行副作用操作。事件绑定,数据请求,动态修改DOM。
useEffect将会在每一次React渲染之后执行。无论是初次挂载时,还是更新。(当然这种行为我们可以控制)
需要清除的effect
在传统的class组件中,通常在componentDidMount中添加对事件的监听。在componentWillUnmount中会清除对事件的监听。
我们需要在不同的生命周期函数中,拆分我们的逻辑。
而effect可以返回一个函数,当react进行清除时, 会执行这个返回的函数。每当执行本次的effect时,都会对上一个effect进行清除。组件卸载时也会执行进行清除。
也就是说,下面的代码中。每一次更新,都会对上一次的effect进行卸载,并执行本次的effect。
effect性能优化
每次执行effect,清除上一次effect可能会造成不必要的性能浪费。我们可以通过effect的第二个参数,控制effect的执行。
第二个参数是useEffect的依赖,只有当依赖发生变化时,useEffect才会更新。
当我们传递传递一个空数组作为依赖时,会告诉React,effect不依赖任何state或者props。我们可以使用此行为模拟componentDidMount或者componentWillUnmount。
⚠️ 请记住使用空数组的effect和componentDidMount是有差异的
?️useEffect(fn, [])和componentDidMount的差异有什么差异?
类似的问题,为什么有时候在effect里拿到的是旧的state或prop呢?
useEffect会捕获props, state,但是始终是初始的值。 如上图所示,当我们点击button多次,effect在3秒之后的回调,打印的依然是state的初始值。
为什么会这样?
这是因为javascript闭包的机制,函数组件被调用后,函数内部的state由于被内部定时器的回调所依赖,所以没有被垃圾回收机制所清除,而是保存在内存里了。所以等定时器的回掉执行时,打印的是之前闭包中存储的变量。
如何避免拿到旧的prop或者state?
- 使用useRef
- 查看是否遗漏了依赖, 导致effect没有更新
?️如何正确的useEffect中请求数据?
阅读这篇文章是一个不错的选择How to fetch data with React Hooks?
如何请求数据?
如何在useEffect中使用async/await?
async函数默认会返回一个Promise对象,而useEffect中,只允许什么都不返回或者返回一个清除函数。控制台中,会触发如下警告
Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect..
解决方案如下?
小节
- 可以使用多个useEffect分离逻辑。在class组件中,我们通常会在componentDidMount中,混杂添加事件绑定,请求数据等多种无关的逻辑。
- 如果只需要useEffect在加载时执行一次,或者卸载组件时执行一次,可以向useEffect传递一个空数组。
- useEffect不支持直接使用async函数
- 如果在useEffect中形成了闭包,将会拿到旧的props,或者state。(这个与Hook无关,与函数组件本身的特性相关)
- useEffect返回的函数,清除函数。会在上一个effect被清除时调用。
?Hook规则
- 只在最顶层使用Hook,不在判断,循环语句中使用Hook
- 只在React函数中使用Hook
?️为什么Hook高度依赖执行顺序?
我们借助preact的源码进行分析,在preact中Hook的存储在组件的私有属性__hooks._list的数组中。读取和存储都依赖currentIndex的指针,如果hook的执行顺序改变,currentIndex也会被改变,获取的hook可能是完成错误的。
⚙️自定义Hook
- 自定义Hook必须以use开头
- 自定义Hook只是逻辑复用,其中的state是不会共享的。
- 自定义Hook内部可以调用其他Hook。
- 避免过早的拆分抽象逻辑
下面是一个自定义Hook的?
?自定义数据获取Hook
我们通过ajax请求表哥数据时,很多逻辑都是通用的。比如loading的状态的处理,错误信息的处理,翻页的处理。我们可以把这些逻辑抽象成一个公共的Hook。不同的api,作为自定义Hook的参数。
下面是一个数据请求自定义Hook的例子:
如何使用?从自定义Hook向外暴露一些state,和setState。当page发生改变时,会重新请求数据。
?其他API
useContext
接收一个context对象,并返回当前的context的值。useContext可以订阅context的变化。但是仍然需要上层组件使用<MyContext.Provider>来为下层组件提供context。
useReducer
useReducer接收三个参数,reducer函数,initialArg初始值,init惰性初始值函数。reducer函数和Redux的reducer类似,接收state,以及action。返回更新后的state。如果传入三个参数,init(initialArg)将作为初始值。
useReducer在复杂场景下比useState更适用。
指定初始state
useReducer的第二个参数可以指定初始的state
惰性初始化
如果指定useReducer的第三个参数,useReducer的初始值会被设置为init(initialArg)
跳过dispatch
如果ReducerHook的返回的state与当前state相同,React将跳过子组件的渲染及effect的执行。
?例子
在自定义Hook的例子中,使用了多个useState进行状态管理,当出现大量状态时,useState会使得逻辑变得很复杂。我们现在可以使用useReducer管理多个state,也可向子组件传递dispatch,而不是回调函数。
下面是将自定义Hook中,请求数据自定义Hook的例子,改造成useReducer的方法。
useCallback
useCallback接收回调函数和依赖数组作为参数。useCallback会返回memoized函数。当依赖项改变的时候,会返回的新的memoized函数。
useMemo
useMemo和useCallback类似。useMemo会返回memoized值。当依赖项改变时,会重新计算memoized值。
?useRef
useRef除了获取dom节点的功能外,useRef的current属性,可以方便保存任何可变值。useRef每一次渲染时,都会返回同一个ref对象。
设想下面?这种情况, 组件重新渲染时,由于timer发生了变化,我们会永远无法清除定时器。
如果想要在更新后,依然可以清除定时器,可以将timer,保存到useRef的current属性上
useLayoutEffect
(红圈中是同步的操作)
useLayoutEffect和useEffect类似,但是不同的是:
- useEffect,使用useEffect不会阻塞浏览器的重绘
- useLayoutEffect, 使用useLayoutEffect,会阻塞浏览器的重绘。如果你需要手动的修改Dom,推荐使用useLayoutEffect。因为如果在useEffect中更新Dom,useEffect不会阻塞重绘,用户可能会看到因为更新导致的闪烁,
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。