Skip to main content

React Hooks

类组件与函数组件

类组件侧重面向对象的编程思想,更注重复用和集成,一系列的功能集合。

函数组件面向函数式的编程思想,封装一个程序体,完成特定功能。

组件做的事情是根据状态渲染特定的UI到浏览器,本质上是一种特定的功能。

render(data) -> UI -> Browser

函数组件和类组件可以互相调用,因为二者本质都返回JSX

类组件

类组件是基于面向对象的思想构建的组件体系,采用面向对象思想,其实是想把更多跟视图相关的功能全部封装在一个容器里面,让组件具有独立性,更好的代码组织。

优点
  1. 具有内部的状态。
  2. 有完整的生命周期。
  3. 有单独的render函数。
缺点
  1. 限制了组件方法逻辑的抽离。
  2. 复杂的this与生命周期函数的使用。
  3. 组件的状态很难复用。

函数组件

函数组件是基于函数式编程构建组件的方式,主要为了增加组织代码的自由度。使用函数组件,是希望跟视图相关的逻辑被抽离出去,使组件变得更加自由,并在此基础上进行复用和集成。

优点
  1. 从组件render的特性出发,执行一次返回一个视图。
  2. 函数式组件工具,可以更加自由的抽离组件逻辑。
  3. 函数式编程对于性能方面的要求相对较小。
缺点
  1. 组件无内部状态。其状态保存在外部。
  2. 生命周期不完整。类组件的一些声明周期无法再函数组件中实现。
  3. 重新执行整个函数触发渲染。

hook

hook意为钩子,一般都是函数,主要帮助函数创建一些外部的数据源,并帮助函数与外界进行沟通联络。

在React中,hook主要是钩到视图相关的数据源,当数据变化时,指示函数重新执行渲染。钩数据源的过程其实就是分析视图代码的过程,让视图与数据相关关联。

总的来说,hook是帮助函数组件在外部建立与UI状态、声明周期等钩子的工具,并且能绑定到视图,在状态改变时更新视图的时机就是hook

特点

  1. 不能在类组件中使用,只能在函数组件中使用。
  2. hook都是以use开头的函数,包括自定义hook
  3. hook应该在函数内部的最顶层示使用。

react hook

  1. useState 创建状态。
  2. useReducer useState的升级版,创建数据与复杂的数据操作。
  3. useEffect 处理副作用。
  4. useContext 创建组件体系的上下文对象。
  5. useMemo 创建可计算的数据。
  6. useCallback 缓存一个方法(函数)。
  7. useRef 获取节点实例。

useState

用来创建一个状态。

initialState 任意类型值作为初始值。如果是一个函数则只会在组件初始化时执行一次并将其结果作为初始值,后续的重新渲染不会执行。

返回一个具有两个元素的数组,第一个元素为当前状态,第二个元素为设置状态的方法(set funtion)。

import { useState } from 'react'

const [state, setState] = useState(initialState)
  • state是只读的,必须通过set funtion进行state的设置,从而触发重新渲染。
  • 设置值的时必须是新值或者新的引用,因为React只会进行浅对比。
  • set funtion可接收一个值或函数,如果是一个函数则上一次的状态作为其唯一的参数。
  • set funtion不返回任何值。

useReducer

useState的升级版。如果一个状态的操作方式有多种,useReducer是最好的选择。

该钩子接收两个参数:

  1. reducer是更新状态的操作集合,为一个函数。
  2. initialState要维护状态的初始值。
import { useReducer } from 'react'

const [state, dispatch] = useReducer(reducer, initialState)

useEffect

useEffect用于处理副作用,可接受两个参数:

  1. 处理副作用的回调。与视图无关的操作,如打印、请求数据、计时器等。
  2. 依赖项数组。依赖项中的依赖改变时回调重新执行。
import { useEffect } from 'react'

useEffect(() => {
// do something
},[deps])

useEffect可用来模拟类组件中的部分生命周期函数:

  1. componentDidMount 组件挂载完成。
  2. componentDidUpdate 组件更新完成。
  3. componentWillUnmount 组件即将卸载。

依赖项

  1. 当依赖项为空数组[]时,回调只会在初次渲染后执行一次:
useEffect(() => {
console.log('Initial Render');
}, [])
  1. 无依赖项时,回调会在每次渲染后执行。这种方式极不安全,应尽量避免使用。
useEffect(() => {
console.log('Component Rendered');
})
  1. 有明确的依赖项时,在初次渲染或当其中任意一个依赖项发生变化时,回调会执行。作为依赖项的标准是回调函数内部需要依赖项参与程序。
const [count, setCount ] = useState(0)

useEffect(() => {
console.log('count is changed')
}, [count])

清除副作用

useEffect的回调函数不能是异步函数(async function),因为回调函数可以返回一个清除副作用的函数。

useEffect(() => {
let t = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
clearInterval(t)
}
}, [])

当项目足够大且复杂的时候,尽量把请求数据封装在redux中,组件每次被调用的时候,都会进行一次数据请求,这大概率是没有意义的。

hook设计

状态持久化

函数组件每次执行时内部的状态都会重新初始化,包括变量、函数等,函数组件想要更新,没有办法绕开重新执行、重新初始化的过程。 因此如果想让函数有保活(keep-alive)的特征,那么就必须将状态存储在函数外部的容器与函数形成一个闭包的关系, 形成持久化的存储,后续更新状态修改容器内部的数据即可。

对于类组件来说,它其实是一个盒子,所有的方法和状态保存在一个单独的组件的对象中,而render只是一系列方法中的一个, 因此不存在当render方法执行以后class重新初始化的问题。组件只要被创建出实例来,在整个应用程序存活的状态下, 没有特殊操作,组件对象不会被销毁。

memo

默认情况下,由于父组件的更新,导致父组件函数重新执行,而在返回的JSX中,有调用子组件的行为,因此子组件也会重新执行。 这样就带来一个问题,即使父组件传递给子组件的props没有变化,子组件都会会重新执行。

memo可以让React记住上一次的子组件的状态,只有当子组件所涉及到的props发生变化时才会重新执行子组件。memo的本质就是一个高阶组件。

import { 
useState,
memo
} from 'react'

// function Child ({ count2 }) {
// console.log('Child rerendered');
// return (
// <h1>Count2: {count2}</h1>
// )
// }

// const Child = memo(({ count2 }) => {
// return (
// <h1>Count2: {count2}</h1>
// )
// })

const Child = memo(({ data }) => {
console.log('Child rerendered');
return (
<h1>Count2: {data.count2}</h1>
)
})

function App() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)

const data = {
count2
}

return (
<div>
<h1>Count1: {count1}</h1>
<button onClick={() => setCount1(count1 + 1)}>count1 +</button>
{/* <Child count2={count2}/> */}
<Child data={data}/>
<button onClick={() => setCount2(count2 + 1)}>count2 +</button>
</div>
)
}

export default App

memo的核心是对引用进行浅比较,如果每次在函数重新执行时,传递的是不同的引用,子组件依然会重新执行。 这是memo无法避免的问题。

useMemo

useMemo接受一个回调函数与依赖项参数:

const data = useMemo(callback, deps)

useMemo使用计算属性的方式缓存值,依赖项更新的时候才会重新执行一次回调,并返回新的值。这种方式保证了依赖项不更新的时候,变量永远得到的是之前的值,起到了缓存数据的作用,以解决组件函数重新执行的问题。

function App() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)

const data = useMemo(() => ({ count2 }), [ count2 ])

return (
<div>
<h1>Count1: {count1}</h1>
<button onClick={() => setCount1(count1 + 1)}>count1 +</button>
<Child data={data}/>
<button onClick={() => setCount2(count2 + 1)}>count2 +</button>
</div>
)
}

useCallback

useCallback可用来缓存一个函数,依赖变更时重新生成一个函数:

const data = useCallback(callback, deps)

其实现的功能与useMemo类似,区别在于useMemo缓存一个值,而useCallback用来缓存一个函数。

useRef

current会在

虚拟DOM生成真实DOM以后,会把DOM对象给到current属性

useContext

useLayoutEffect

useEffect处理异步任务比较多。因为异步任务耗时,如果在物理渲染视图之前执行回调,异步任务会阻塞渲染, 因为事件环的顺序是先清空微任务完毕后才会做GUI渲染,这种情况会使白屏时间变长。

  1. state已准备
  2. 开始解析虚拟DOM
  3. 对比新旧虚拟DOM树
  4. 组装真实DOM
  5. 物理渲染DOM,数据等待阶段
  6. useEffect回调执行
  7. 数据到位,异步请求
  8. 更新虚拟DOM
  9. 真实DOM补丁
  10. 更新渲染

useLayoutEffect处理同步任务较多,如DOM操作等。useLayoutEffect常与useRef配置使用。 useRef拿到DOM对象进行操作,是在GUI渲染之前进行,也就是DOM并没有进行再次的物理渲染的情况下 进行了手动DOM操作,当GUI渲染时,DOM的更新需求已经完成了。