Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks 使用总结 #120

Open
yangtao2o opened this issue Mar 21, 2021 · 0 comments
Open

React Hooks 使用总结 #120

yangtao2o opened this issue Mar 21, 2021 · 0 comments
Assignees

Comments

@yangtao2o
Copy link
Owner

yangtao2o commented Mar 21, 2021

React Hooks 使用总结

组件类的缺点

  1. 大型组件很难拆分和重构,也很难测试。
  2. 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  3. 组件类引入了复杂的编程模式,比如 render props 和高阶组件

函数组件

  1. 数据流的管道,不是复杂的容器
  2. 组件的最佳写法应该是函数,而不是类
  3. 必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类

类和纯函数

  • 类:数据和逻辑的封装

  • 纯函数:只应该做一件事,就是返回一个值

    • 函数返回结果只依赖参数
    • 函数执行不会对外产生可观察的变化
  • 副作用:数据计算无关的操作

    • 如:生成日志、储存数据、改变应用状态等

Hook

目的:

  • React 函数组件的副作用解决方案
  • 加强版函数组件,完全不使用"类",可写出一个全功能的组件

含义:

  • 组件尽量写成纯函数
  • 当需要外部功能和副作用的时候,就用钩子把外部代码"钩"进来

Hook 方法

  1. useState - 数据存储,派发更新
  2. useEffect - 组件更新副作用钩子
  3. useRef - 获取元素 ,缓存数据
  4. useContext - 自由获取 context
  5. useReducer - 无状态组件中的 redux
  6. useMemo - 小而香性能优化
  7. useCallback - useMemo 版本的回调函数
  8. useLayoutEffect - 渲染更新之前的 useEffect

useState

特点:

  • useState 派发更新函数的执行,会使 function 组件从头到尾执行一次
  • 可以配合 useMemo,usecallback 等 api 配合使用,起到优化作用

使用:

import { useState } from 'react'

export default function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0)

  console.log('outer', count) // 会及时更新

  const handleClick = () => {
    setCount(count + 1)
    // 只有当下一次上下文执行的时候,state值才随之改变
    console.log('inner', count) // 不会及时更新
  }
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

useEffect

特点:

  • useEffect 第一个参数不能直接用 async await 语法,可以在内部调用
  • 第二个参数是个数组,可作为限定条件,限制 useEffect 的执行
  • 如果没有第二个参数,useEffect 会受 state 或 props 更新而执行
  • return 可清除 effect

与 useLayoutEffect 的执行过程对比:

  • 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调
  • 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器 dom 绘制完成

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种:

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

注意:如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起。

使用搜索功能体验下各种状态是如何处理的,详细内容讲解,见这里[译] 如何使用 React hooks 获取 api 接口数据

主要包含:

  • 获取数据,初次自动加载,以及表单搜索也可重新发起请求
  • 使用 Loading
  • 添加错误处理
  • 添加中止数据请求,可防止切换组件,因找不到组件而触发警告
import { useState, useEffect } from 'react'
import axios from 'axios'

export default function Example() {
  const [data, setData] = useState({ items: [] })
  const [target, setTarget] = useState('javascript')
  const [isLoading, setIsLoading] = useState(false)
  const [isError, setIsError] = useState(false)
  const [url, setUrl] = useState(
    'https://api.github.com/search/repositories?sort=stars&q=javascript'
  )

  useEffect(() => {
    let didCancel = false
    const fetchData = async () => {
      setIsError(false)
      setIsLoading(true)
      try {
        const res = await axios(url)
        if (!didCancel && res?.data) {
          setData(res.data)
        }
      } catch (e) {
        if (!didCancel) {
          setIsError(true)
        }
      }
      setIsLoading(false)
    }

    fetchData()

    return () => {
      didCancel = true
    }
  }, [url])

  const handleClick = () => {
    const url = `https://api.github.com/search/repositories?sort=stars&q=${target}`
    setUrl(url)
  }

  return (
    <>
      <input
        type="text"
        value={target}
        onChange={event => setTarget(event.target.value)}
      />
      <button type="button" onClick={handleClick}>
        Search
      </button>
      {isError && <div>出错了...</div>}
      {isLoading ? (
        <div>加载中...</div>
      ) : data.items.length > 0 ? (
        <ul>
          {data.items.map(item => (
            <li key={item.id}>
              <a href={item.html_url}>{item.name}</a>
            </li>
          ))}
        </ul>
      ) : (
        <div>没有更多信息了...</div>
      )}
    </>
  )
}

为什么要在 effect 中返回一个函数?这是 effect 可选的清除机制。

React 何时清除 effect?

  • React 会在组件卸载的时候执行清除操作。
  • effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

接下来模拟下实际项目中倒计时的写法,使用了 setTimeout 模拟 setInterval,当然也使用到了清除操作。

import { useState, useEffect } from 'react'

const STATUS = {
  STOP: 'stop',
  START: 'start',
  TIMEOUT: 1000,
  MAXTIME: 9,
}

export default function Example() {
  const [time, setTime] = useState(STATUS.MAXTIME)
  const [status, setStatus] = useState(STATUS.STOP)

  useEffect(() => {
    let timerId = null
    // 主体运行函数
    const run = () => {
      if (time <= 1) {
        setTime(STATUS.MAXTIME)
        setStatus(STATUS.STOP)
        return
      }
      setTime(time => time - 1)
      // 回调
      timerId = setTimeout(run, STATUS.TIMEOUT)
    }

    // 根据操作行为切换定时器状态
    if (status === STATUS.START) {
      timerId = setTimeout(run, STATUS.TIMEOUT)
    } else {
      timerId && clearTimeout(timerId)
    }

    // eefect 清除操作
    return () => {
      timerId && clearTimeout(timerId)
    }
  }, [status, time])

  const handleClick = e => setStatus(e.target.value)

  return (
    <div>
      <p>倒计时:{time}</p>
      <button type="button" value={STATUS.START} onClick={handleClick}>
        开始
      </button>
      <button type="button" value={STATUS.STOP} onClick={handleClick}>
        暂停
      </button>
    </div>
  )
}

useRef

特点:

  • 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数
  • 返回的 ref 对象在组件的整个生命周期内保持不变,可缓存数据
  • useRef 会在每次渲染时返回同一个 ref 对象
  • 想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现

我们在 class 中通过 ref 属性来访问 DOM,然而useRef()比 ref 属性更好用 —— useRef()可以很方便地保存任何可变值。

访问 DOM:

import { useRef } from 'react'

export default function Example() {
  const inputEl = useRef(null)

  const onButtonClick = () => {
    inputEl.current.focus() // `current` 指向已挂载到 DOM 上的文本输入元素
  }

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  )
}

缓存数据:useRef 可以第一个参数可以用来初始化保存数据,这些数据可以在 current 属性上获取到 ,当然我们也可以通过对 current 赋值新的数据源。

// 初始化
const currenRef = useRef(InitialData)
// 获取
const getCurrentData = currenRef.current
// 更改
currenRef.current = newData

封装一个 usePrevious:

import { useRef, useEffect } from 'react'
const usePrevious = value => {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  }, value)
  return ref.current
}

还可以用在定时器:

function Demo() {
  const [count, setCount] = useState(0)
  const [isClear, setClear] = useState(false)
  const timerID = useRef()

  useEffect(() => {
    timerID.current = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    return () => clearInterval(timerID.current)
  }, [count])

  useEffect(() => {
    return () => clearInterval(timerID.current)
  }, [isClear])
}

由于 usestate,useReducer 执行更新数据源的函数,会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,那我们使用 useRef,就可以既想要保留数据,又不想触发函数的更新。

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

useReducer

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

特点:

  • useState 的替代方案
  • 数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数
  • dispatch 的触发会触发组件的更新
  • 使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

使用:

import { useReducer } from 'react'

const DECREMENT = 'decrement'
const INCREMENT = 'increment'
const RESET = 'reset'

const initialState = { number: 0 }

function reducer(state, action) {
  const { type, payload } = action
  switch (type) {
    case DECREMENT:
      return { number: state.number + 1 }
    case INCREMENT:
      return { number: state.number - 1 }
    case RESET:
      return { number: payload.number }
    default:
      return { number: state.number }
  }
}

export default function Example() {
  const [state, dispath] = useReducer(reducer, initialState)
  return (
    <div>
      当前值:{state.number}
      <button onClick={() => dispath({ type: DECREMENT })}>增加</button>
      <button onClick={() => dispath({ type: INCREMENT })}>减少</button>
      <button onClick={() => dispath({ type: RESET, payload: initialState })}>
        重置
      </button>
    </div>
  )
}

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • 第二个参数是一个 deps 数组,数组里的参数变化决定了 useMemo 是否更新回调函数
  • 如果把 memo 比做无状态组件的 ShouldUpdate,那么 useMemo 就是更为细小的 ShouldUpdate 单元

优点

  • useMemo 可以减少不必要的循环,减少不必要的渲染
  • useMemo 可以减少子组件的渲染次数
  • useMemo 让函数在某个依赖项改变的时候才运行,这可以避免很多不必要的开销

比如我们使用防抖函数时需要这样做:

const searchDebounce = useMemo(() => {
  return debounce(handleSearch, 600)
}, [handleSearch])

这样的话,用 useMemo 包裹之后的 debounce 函数可以避免了每次组件更新再重新声明,可以限制上下文的执行。

useCallback

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])
  • useCallback(fn, []) 相当于 useMemo(() => fn, [])
  • 用处:当以 props 的形式传递给子组件时, 可避免非必要渲染
  • 区别: useMemo 返回的是函数运行的结果,useCallback 返回的是函数
  • useCallback ,需要搭配 react.memo 或 pureComponent 一起使用,才能使性能达到最佳

useMemo 和 useCallBack 示例

接下来的组件中,我们维护了两个 state,可以看到 getCount 的计算仅仅跟 count 有关,那么我们兵分三路,逐个了解下各自的军情。

import React, { useState, useMemo, useCallback } from 'react'

const Child = React.memo(function ({ getCount }) {
  return <h4>传过来的Count值:{getCount()}</h4>
})

export default function DemoUseMemo() {
  const [count, setCount] = useState(1)
  const [val, setValue] = useState('')

  // 普通调用
  const getCount = () => {
    console.log('normal-result')
    return count
  }

  // 使用 useMemo
  const getCountWithMemo = useMemo(() => {
    console.log('useMemo-result')
    return count
  }, [count])

  // 使用 useCallback
  const getCountWithCallback = useCallback(() => {
    console.log('useCallback-result')
    return count
  }, [count])

  return (
    <div>
      <h4>
        Count:{getCount()}, {getCountWithMemo}
      </h4>
      <Child getCount={getCountWithCallback} />
      <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        <input value={val} onChange={event => setValue(event.target.value)} />
      </div>
    </div>
  )
}

当我们点击 +1 时,打印如下:

useMemo-result
normal-result
useCallback-result

当我们输入时,打印如下,只有普通调用的打印结果:

normal-result

如上所示,普通调用时,无论是 count 还是 val 变化,都会导致 getCount 重新计算,所以这里我们希望 val 修改的时候,不需要再次计算,这种情况下我们可以使用 useMemo。

同样的,使用了 useCallback 后,结合 React.memo,显示结果和 useMemo 完全一致。如果这里值使用了 useCallback,而并未使用React.memo,结果如何呢?答案就是和普通调用结果一致。

那 useMemo 和 useCallback 到底有什么异同呢?

  • 相同:接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值
  • 区别:useMemo 返回的是函数运行的结果,useCallback 返回的是函数。

自定义 Hook

特点:

  • 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
  • 自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性

规则:

  • 必须以 “use” 开头
  • 两个组件中使用相同的 Hook 不会共享 state
  • 每次调用 Hook,它都会获取独立的 state

注意:一个好用的自定义 hooks,一定要配合 useMemo, useCallback 等 api 一起使用

自定义获取数据的 Reducer Hook:

import { useState, useEffect, useReducer } from 'react'

const REQUEST_INIT = Symbol('REQUEST_INIT')
const REQUEST_SUCCESS = Symbol('REQUEST_SUCCESS')
const REQUEST_FAILURE = Symbol('REQUEST_FAILURE')

const requestReducer = (state, action) => {
  switch (action.type) {
    case REQUEST_INIT:
      return {
        ...state,
        isLoading: true,
        isError: false,
      }
    case REQUEST_SUCCESS:
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      }
    case REQUEST_FAILURE:
      return {
        ...state,
        isLoading: false,
        isError: true,
      }
    default:
      return console.error('出错了')
  }
}

// 请求数据、更新数据
export function useRequest(cb, isRequest) {
  const [isUpdate, setUpdate] = useState(false)
  const [param, setParam] = useState({})

  const [state, dispatch] = useReducer(requestReducer, {
    isLoading: false,
    isError: false,
    data: '',
  })

  useEffect(() => {
    let didCancel = false
    // 如果是 -1 初次不需要请求
    if (isRequest === -1) {
      return dispatch({ type: REQUEST_SUCCESS })
    }
    const requestData = async params => {
      dispatch({ type: REQUEST_INIT })
      try {
        const res = await cb(params)
        if (!didCancel) {
          dispatch({ type: REQUEST_SUCCESS, payload: res.data })
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: REQUEST_FAILURE })
        }
      }
    }

    requestData(param)

    return () => {
      didCancel = true
    }
  }, [isUpdate, param])

  const isUpdateHandle = () => setUpdate(!isUpdate)
  const onUpdateHandle = param => setParam(param)

  return { ...state, isUpdateHandle, onUpdateHandle }
}

Hook 规则

  • 本质: JavaScript 函数
  • 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook

参考资料

@yangtao2o yangtao2o self-assigned this Mar 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant