(1)class组件的不足
- 状态逻辑难复用:
在组件之间复用状态逻辑很难,例如同一个生命周期可能会掺杂多种不相关的业务逻辑或者同一种业务逻辑会分配到不同的生命周期了。虽然可以通过 render props (渲染属性)或者 HOC(高阶组件)方案来优化此种问题,但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余趋向复杂难以维护:
- 在生命周期函数中混杂不相干的逻辑:
在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
- this 指向问题:
父组件给子组件传递函数时,必须绑定 this
(2)hooks的优势
- 能优化类组件的三大问题
- 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
- 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
- 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
- 只能在 或者中调用 Hook,不要在其他 Javascript 函数中调用
useState 会返回一个数组:一个 state,一个用于更新 state 的函数
useState 唯一的参数就是初始 state,在初始化渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同,
注意事项:
- React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。因为react是根据每个useState定义时的顺序来确定你在更新State值时更新的是哪个state
- 你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
- initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
- Hook 内部使用 Object.is 来比较新/旧 state 是否相等,与 class 组件中的 setState 方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染
- useState不能直接存储函数或函数组件,他会调用该函数并且将函数的返回值作为最终state值进行存储或更新。如果必须这么做可以作为一个数组的元素或对象的某个属性进行存储
- useState没有设置类似class组件中的setState的回调函数来拿到最新state然后进行后续操作;可通过useEffect并设置相应依赖来实现。因为useEffect就是在渲染完成后调用的
useState在异步操作中的状态不同步问题:
函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面。当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。比如下面的一个例子:
当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。
这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxx和this.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。
除了setTimout这种异步,还有类似事件监听函数(比如滚动监听的回调函数)中访问State也会是旧的
这个问题目前的普遍解决方案是使用(见下方)
什么是React中的副作用操作?
指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
副作用操作可以分两类:需要清除的(例如事件绑定/解绑)和不需要清除的。
原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
一个需求实现:需要实时让document.title显示你最新的点击次数(coutnt)
class组件实现:
因为需要实时让document.title显示你最新的点击次数(coutnt),所以就必须在componentDidMount 或 componentDidUpdate中编写重复的代码来重新设置document.title
hooks实现:
useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 、 和 具有相同的用途,只不过被合并成了一个 API
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
4.1 清除副作用
useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
默认情况下,useEffect在第一次渲染之后和每次更新之后都会执行。而useEffect 接收的函数参数所返回的清除副作用的函数则会在组件更新和卸载前执行,然后更新后执行useEffect 接收的函数。然后等待下一次组件更新或卸载,执行清除副作用的函数…如此循环往复
4.2 跳过 effect 进行性能优化
默认情况下,useEffect在每次更新之后都会执行
有时候,我们只想useEffect只在组件挂载时执行,然后卸载时执行清除副作用函数,不想更新时也执行useEffect(比如原生事件的绑定/解绑)或者只想让指定state发生更新时才执行useEffect(比如某些state更新后拿到最新state进行后续操作)
此时,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
- 如果指定state发生更新时才执行useEffect,可以传递一个包含指定state元素的的数组作为第二个参数
注意:无论你是否指定了useEffct的第二个参数,useEffect永远都会在组件挂载时执行一次
4.3 使用多个useEffect
useEffect可以声明多个,React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
我们可以根据具体副作用操作的性质分类将不同种类的操作放到多个useEffect中
4.4 useEffect 不能接收 async 作为回调函数
在useEffect中发起异步请求是很常见的场景,由于异步请求通常都是封装好的异步方法,所以新手很容易写成下面这样:
更优雅的写法:
与useEffect的区别
- useEffect 是异步非阻塞调用,useLayoutEffect 是同步阻塞调用
- useEffect 在浏览器绘制后调用(即Renderer commit阶段结束后),useLayoutEffect 在 DOM 变更(React 的更新)后,浏览器绘制前完成所有操作(即Renderer commit阶段的Layout子阶段同步执行)
使用场景:
大部分情况下使用useEffect即可。针对小部分特殊情况如短时间内触发了多次状态更新导致渲染多次以致屏幕闪烁的情况,使用useLayoutEffect会在浏览器渲染之前同步更新 DOM 数据,哪怕是多次的操作,也会在渲染前一次性处理完,再交给浏览器绘制。这样不会导致闪屏现象发生。
useReducer 和 redux 中 reducer 很像。useState 内部就是靠 useReducer 来实现的
useReducer接收两个参数:reducer函数(含preState和action两个参数)和初始化的state。
选择useReducer还是useState:
- 如果你的页面state很简单,可以直接使用useState
- 如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
- 对于复杂的state操作逻辑(比如某项操作同时需要操作或更新多个state值),嵌套的state的对象,推荐使用useReducer
- 如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
useContext(MyContext) 相当于 class 组件中的 或者
使用useContext和useReducer模拟实现简易Redux:
Provider组件:
Component A:
Component B:
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个。 也就是说,useRef的更新不会引起当前组件或子组件的更新渲染
但使用 React.createRef ,每次重新渲染组件都会重新创建 ref
示例:用useRef存储dom节点
用useRef解决useState异步更新不同步的问题:
针对上面useState谈到的异步更新不同步的问题,用useRef返回的immutable RefObject(把值保存在current属性上)来保存state,。然后取值方式从counter变成了: counterRef.current。如下:
React.forwardRef
在useRef出来之前,由于函数组件是没有实例的,所以函数组件无法使用ref属性来获取dom引用,而对应的解决方法就是React.forwardRef:
TextInput函数组件:
上面例子说明forwardRef和useRef配合 可以在父组件中操作子组件的 ref 对象
useCallback缓存一个函数,这个函数如果是由父组件作为props传递给子组件,或者自定义hooks里面的函数【通常自定义hooks里面的函数不会依赖于引用它的组件里面的数据】,这时候我们可以考虑缓存这个函数,好处:
- 不用每次重新声明新的函数,避免释放内存、分配内存的计算资源浪费
- 子组件不会因为这个函数的变动重新渲染。【和React.memo搭配使用】
上面例子,将一个函数交给useCallBack处理并且作为props传递给memo包裹的子组件并子组件调用该方法,定义只有当coutn变化时才会触发子组件重新渲染
因为通过useCallBack包裹后的函数通过props传递给子组件的永远是该函数的引用
useMemo 主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。
useMemo的返回值就是计算函数的返回值
上面例子只有当count变化时才会触发getNum函数的重新计算和渲染;如果不使用useMemo则任何一个state发生变化都会导致组件重新渲染进而导致getNum重新计算,耗费性能
useMemo和useCallback的区别 useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
共同作用:
- 仅仅 依赖数据 发生变化,才会重新计算结果,也就是起到缓存的作用。
两者区别:
- useMemo 计算结果是计算函数返回来的值,主要用于 缓存计算结果的值,应用场景如: 需要计算的状态
- useCallback计算结果是计算函数,主要用于 缓存函数,应用场景如:需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
注意:当父组件更新渲染(state或props变化)时,无论子组件的props是否改变都会默认更新渲染子组件。所以这种情况下,若你的useMemo或useCallback是用来传给子组件的props时,都必须借助React.memo来包裹子组件完成缓存,才能避免子组件的无效多余更新
必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
(1)useDidMount
(2)useWillUnmount
useEffect 时已经提及过,其允许返回一个 清除副作用的 函数,当依赖项为[]时,其相当于componentWillUnMount
(3)实现类似class组件可支持回调的setState方法
class组件更新状态时,setState可以通过第二个参数拿到更新完毕后的回调函数。很遗憾,虽然hooks函数的useState第二个参数回调支持类似class组件的setState的第一个参数的用法(通过传入一个函数并将函数的返回值作为新的state进行更新),但不支持第二个参数回调,但是很多业务场景中我们又希望hooks组件能支持更新后的回调这一方法。
借助useRef和useEffect配合useState来实现这一功能:
说明:
利用useRef的特性来作为标识区分是挂载还是更新,当执行setXstate时,会传入和setState一模一样的参数,并且将回调赋值给useRef的current属性,这样在更新完成时,我们手动调用current即可实现更新后的回调这一功能
没有 Hooks 之前,高阶组件和 Render Props 本质上都是将复用逻辑提升到父组件中。而 Hooks 出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件中。这样就能够避免 HOC 和 Render Props 带来的「嵌套地狱」。但是,像 Context 的 和 这样有父子层级关系(树状结构关系)的,还是只能使用 Render Props 或者 HOC。
对于 Hooks、Render Props 和高阶组件来说,它们都有各自的使用场景:
Hooks:
替代 Class 的大部分用例,除了 getSnapshotBeforeUpdate 和 componentDidCatch 还不支持。 可提取复用逻辑。除了有明确父子关系的,其他场景都可以使用 Hooks。
Render Props:
在组件渲染上拥有更高的自由度,可以根据父组件提供的数据进行动态渲染。适合有明确父子关系的场景。
高阶组件:
适合用来做注入,并且生成一个新的可复用组件。适合用来写插件。
不过,能使用 Hooks 的场景还是应该优先使用 Hooks,其次才是 Render Props 和 HOC。当然,Hooks、Render Props 和 HOC 不是对立的关系。我们既可以用 Hook 来写 Render Props 和 HOC,也可以在 HOC 中使用 Render Props 和 Hooks。
参考文章:
https://juejin.cn/post/6844903985338400782