React Hooks

226 阅读10分钟

在react 16.8版本中增加了 Hooks。

Hooks 是什么?

Hooks 直译 “钩子”,主要作用有如下几点:

  • 对函数型组件进⾏增强
  • 让函数型组件可以存储状态
  • 可以拥有处理副作⽤的能⼒

让开发者在不使⽤类组件的情况下, 在函数组件中实现相同的功能。 React 官方是提倡使用函数组件,因此学习Hooks是很有必要性的

什么是副作用?只要不是把数据转换成视图的代码,它就是副作用。比如说:获取DOM元素,为DOM元素设置事件, 设置定时器, 发起ajax请求等等都属于副作用。在类组件里,经常通过生命周期函数来处理这些副作用

类组件的不足

  1. 缺少逻辑复用机制

为了复⽤逻辑增加⽆实际渲染效果的组件,增加了组件层级 显示⼗分臃 增加了调试的难度以及运⾏效率的降低

  1. 类组件经常会变得很复杂难以维护

将⼀组相⼲的业务逻辑拆分到了多个⽣命周期函数中,在⼀个⽣命周期函数内存在多个不相⼲的业务逻辑

  1. 类成员⽅法不能保证this指向的正确性

React Hooks 的使用

Hooks 意为钩⼦, React Hooks 就是⼀堆钩⼦函数, React 通过这些钩⼦函数对函数型组件进⾏增强, 不同的钩⼦函数提供了不同的功能.

  • useState()
  • useEffects()
  • useReducer()
  • useRef()
  • useCallback()
  • useContext()
  • useMemo()

useState

它的使用以及和类组件的对比可访问官网有详细的说明和介绍,此处不做过多的介绍。

和类组件中使用方式对比, 使用函数组件的优点有:

  • 代码更简洁,代码量更少了
  • 可以不使用this

useState 的使用示例:

// 引入钩子函数 useState
import { useState } from "react";
function App() {
  // 调用useState,为count添加初始值
  // 返回 count的初始值,和 setCount 的函数用于更新
  const [ count, setCount ] = useState(0)
  return (
    <div className="App">
      <p> You  clicked {count} times </p>
      <button onClick =  {() => setCount(count + 1)}>click me</button>
    </div>
  )
}

从上面的代码中我们可以得出以下两点:

  1. 接收唯⼀的参数即状态初始值. 初始值可以是任意数据类型.

  2. 返回值为数组. 数组中存储状态值和更改状态值的⽅法. ⽅法名称约定以set开头, 后⾯加上状态名称.

  3. ⽅法可以被调⽤多次. ⽤以保存不同状态值.


function App() {
  // 调用useState,为count添加初始值
  // 返回 count的初始值,和 setCount 的函数用于更新
  const [ count, setCount ] = useState(0)
  // 再次调用useState, 创建一个新的对象状态
  const [ person, setPerson ] = useState({name: 'huan_zai', age: 18})
  return (
    <div className="App">
      <p> You  clicked {count} times </p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      <p>UserName: {person.name} Age: {person.age}</p>
      <button onClick={() => setPerson({name: 'linhuan', age: 18})} >change name</button>
    </div>
  )
}
  1. 参数可以是⼀个函数, 函数返回什么, 初始状态就是什么, 函数只会被调⽤⼀次, ⽤在初始值是动态值的情况.
const [ count, setCount ] = useState(() => 100)

获取动态值作为初始值

const propsCount = props.count || 0
const [ count, setCount ] = useState(propsCount)

虽然,上面的代码还是会实现效果,但是第一行代码,在每一次count 更新,组件重新渲染的时候,这一行代码都会被执行,这样做是没有意义的,因此我们可以给useState 传入一个函数作为参数,函数只会执行一次

const [ count, setCount ] = useState(() => {
    return props.count || 0
  })

关于设置状态值方法 setXXX 的两点细节:

  1. 设置状态值⽅法的参数可以是⼀个值也可以是⼀个函数

function handleCount() {
    // 现在的setCount的参数是一函数
    setCount(count => count + 1)
  }
  // 原先的setCount的参数是一个值
  <button onClick={() => setCount(count + 1)}>click me</button>
  // 现在的
  <button onClick={handleCount}>click me</button>
  1. 设置状态值⽅法的⽅法本身是异步的
function handleCount() {
    setCount(count => count + 1)
    document.title = count
  }

image.png

当点击更新count的值, 而 title的值还是上一次的旧的值,因此可见设置状态值⽅法的⽅法本身是异步的

优化一下功能,使title 和 count 同步

function handleCount() {
    setCount(count => { 
      var newCount = count + 1
      document.title = newCount
      return newCount
    })
    
  }

image.png

手写 useState 的实现

根据上文中的useState 的使用,接下来一起来一步步实现它。

首先, 我们可以根据它的入参和返回值,来搭建一个基本的架子:

function useState(initialState) {
  // 接收初始状态
  let state = initialState
  // 更新状态
  let setState = newState => {
    state = newState
    // 重新渲染视图
    render()
  }
  return [state, setState]
}
import  ReactDOM from "react-dom";

function render() {
  ReactDOM.render(<App />, document.getElementById('root'))
}

基本的架子搭好后,我们可以尝试的调用一下,看是否可行。

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <p> You  clicked {count} times </p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      
    </div>
  )
}

【结果】: 点击按钮, count 没有 增加还是保持0不变。

【分析】:我们从点击按钮开始的流程是, setCount -> setState -> render 引起组件的重新渲染 -> App() 重新调用 -> useState 会被重新调用 -> state 被重新赋值成 0

【解决】 将state的声明从原来useState内部提到外部去

let state = null
function useState(initialState) {
  // 接收初始状态
  state = state || initialState
  // 更新状态
  let setState = newState => {
    state = newState
    // 重新渲染视图
    render()
  }
  return [state, setState]
}

到目前为止,只是实现了基本的单次的调用,那多次的调用,上面的代码就显得力不从心了,因此,接下来要对上面的代码进行完善,支持多次的调用。

我们对于useState 要被调用几次,根本是无法确定的,因此我们只能借助数组来存放所有的状态和对应的更新方法

// 存放状态
let state = []
// 存放状态对应的更新方法
let setters = []
// 通过索引将状态对应的更新方法对应起来
let stateIndex = 0

对索引创建一个产生闭包的的方法,保证了状态和其对应的更新方法能够准确地对应起来

function createSetter(index) {
  return function (newState) {
    state[index] = newState
    render()
  }
}

重新渲染后要将索引重置,否则索引一直累加会超出数组的范围

function render() {
  // 重新渲染后要将索引重置,否则索引一直累加会超出数组的范围
  stateIndex = 0
  ReactDOM.render(<App />, document.getElementById('root'))
}

完善后的完整代码:

import './App.css';
import  ReactDOM from "react-dom";
// 存放状态
let state = []
// 存放状态对应的更新方法
let setters = []
// 通过索引将状态对应的更新方法对应起来
let stateIndex = 0 
function useState(initialState) {
  
  state[stateIndex] = state[stateIndex] || initialState
  setters.push(createSetter(stateIndex))
  let value = state[stateIndex]
  let setState = setters[stateIndex]
  stateIndex++
  return [value, setState]
}
function render() {
  // 重新渲染后要将索引重置,否则索引一直累加会超出数组的范围
  stateIndex = 0
   // 清空数据,否则数组会重复变多
  setters = []
  ReactDOM.render(<App />, document.getElementById('root'))
}
function createSetter(index) {
  return function (newState) {
    state[index] = newState
    render()
  }
}
function App() {
  const [count, setCount] = useState(0)
  // const [person, setPerson] = useState({name: 'huan_zai', age: '18'})
  const [name, setName] = useState('huan_zai')
  return (
    <div className="App">
      <p> You  clicked {count} times </p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      <p>UserName: {name} </p>
      <button onClick={() => setName('linhuan')} >change name</button>
    </div>
  )
}

export default App;

开始调试,于是打开界面

image.png

我们点击 按钮 change Name 会发现,点击一次按钮,文本userName 未改变,需要再次点击才会。尝试 点击按钮 chenge person 也是这样的。这是怎么回事呢?我通过控制台打印console.log(setters)和断点调试发现:

image.png image.png

oh! my god! 这是我生平第一次见到这么奇怪的事儿! 打印结果的数组没有展开的时候是3项,展开后,里面竟然出现了 6项 !!!经过一番的调试,我就是发现App() 方法会被调用两次,出现这个的原因竟然是:

image.png

打开了严格模式, 严格模式下, App 方法会被调用两次,即渲染两次

useContext

作用: 在跨组件层级获取数据时简化获取数据的代码

image.png

useEffect

作用: 让函数型组件拥有处理副作用的能力, 类似类的生命周期函数

执行时机: 可以把useEffect 这个函数看作是componentDidMount, componentDidUpdate 和 componentWillUnmount这三个函数的组合

useEffect (() => {}) 类比 => componentDidMount, componentDidUpdate

useEffect (() => {}, []) 类比 => componentDidMount

useEffect (() => () => {}) 类比 => componentWillUnmount

案例:

import './App.css';
import { useEffect, useState } from "react";
import  ReactDOM from "react-dom";

function App() {
  function onscroll() {
    console.log('页面发生滚动')
  }
  // 1. 为window对象添加滚动事件
  // 挂载完成后添加事件,组件卸载前销毁事件 
  useEffect(() => {
    window.addEventListener('scroll', onscroll )
    return () => {
      window.removeEventListener('scroll', onscroll)
    }
  }, [])
  //  2. 设置一个定时器,让count数值每隔一秒加 1
  const [count, setCount] = useState(0)
  useEffect(() => {
   const timeId = setInterval(() => {
     setCount(count => {
      document.title = count 
      return count + 1
     }) 
   }, 1000)
   return () => {
     clearInterval(timeId)
   }

  })
  return (
    <div className="App">
      <span>{ count }</span>
      <button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
    </div>
  )
}
export default App;

useEffect解决的问题

  1. 按照用途将代码进行分类(将一组相干的逻辑归置到了同一个副作用函数中)

  2. 简化重复代码,使组件内部代码更加清晰。useEffect (() => {}) 类比 => componentDidMount, componentDidUpdate , 原先的类组件要使用两个生命周期的API 才能完成挂载和更新, 在这里只要使用useEffect (() => {})

useEffect 的第二个参数

作用:

  • 第二个参数是空数组, 表示副作用函数只会在挂载完成后执行一次, 此时不会监测更新
  • 第二个参数指定了一个有值的数组, 表示只有指定数据发生变化时才会执行副作用函数

useEffect 结合异步函数

useEffect中的参数函数不能是异步函数, 因为useEffect函数要返回清理资源的函数, 如果是异步函数就变成了返回Promise

image.png

手写useEffect 实现

首先,来个基础版本的,主要实现功能:

  • useEffect 只有一个参数(回调函数)的情况下, 即调用该回调函数
  • useEffect 传入第二个参数时, 要监测其数据变化时才执行回调函数
// 保存上次的数据
let preArray = [] 
function useEffect(callback, depsArray) {
  // callback 是否是函数
  if(typeof callback !== 'function') {
    throw new Error('first params must be function!!')
  }
  // 判断depsArray有没有被传递
  if(typeof depsArray === 'undefined') {
    callback()
  } else {
    // 判断是不是数组
    if(Object.prototype.toString.call(depsArray) !== '[object Array]') throw new Error('second params must be Array!')
    // 当前的依赖数组和上一次的进行对比,看是否发生变化
    // 有变化调用回调函数
    const hasChange = preArray.length === 0 || depsArray.every((dep, index) => dep === preArray[index]) === false
    if(hasChange) {
      callback()
    }
    // 同步依赖值
    preArray = depsArray
  }
}

测试一下

function App() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('huan_zai')
  const [person, setPerson] = useState({name: 'huan_zai', age: '18'})
  useEffect(() => {
    console.log('hai')
  }, [count])
  return (
    <div className="App">
      <p> You  clicked {count} times </p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      <p>UserName: {name} </p>
      <button onClick={() => setName('linhuan')} >change name</button>
      <p>UserName: {person.name} Age: {person.age}</p>
      <button onClick={() => setPerson({name: 'linhuan', age: '28'})} >change person</button>
    </div>
  )
}

结果, 在只有点击 click me 按钮时才会触发useState 的副作用函数, 符合预期结果。

但是,但我们想要对多个不同数据进行监测的时候, 上面的代码就无法实现

例如:

useEffect(() => {
    console.log('hai')
  }, [count])
  useEffect(() => {
    console.log('hai name')
  }, [name])

结果:无论是哪个数据发生变化, useEffect 都会被执行, 这就有问题了,接下来我们需要优化下代码

image.png

优化后的代码:


// 保存上次的数据
let preDepsArray = []
let effectIndex = 0
function useEffect(callback, depsArray) {
  // callback 是否是函数
  if(typeof callback !== 'function') {
    throw new Error('first params must be function!!')
  }
  // 判断depsArray有没有被传递
  if(typeof depsArray === 'undefined') {
    callback()
  } else {
    // 判断是不是数组
    if(Object.prototype.toString.call(depsArray) !== '[object Array]') throw new Error('second params must be Array!')
    // 当前的依赖数组和上一次的进行对比,看是否发生变化
    // 有变化调用回调函数
    const preDep = preDepsArray[effectIndex]
    let hasChange = preDep ? depsArray.every((dep, index) => dep === preDep[index]) === false : false

    if(hasChange) {
      callback()
    }
    // 同步依赖值
    preDepsArray[effectIndex] = depsArray
    effectIndex++
  }
}

手写useReducer 实现

function useReducer(reducer, initialState) {
  const [ state, setState ] = useState(initialState)
  function dispatch(action) {
    const newState = reducer(state, action)
    setState(newState)
  }
  return [state, dispatch]
}


function App() {
 
  function reducer (state, action) {
    switch (action.type) {
      case 'increment':
          return state + 1
      case 'decrement':
          return state - 1
      default:
          return state
    }
  }
  const [count, dispatch] = useReducer(reducer, 0)

  return (
    <div className="App">
      <p> {count}  </p>
      <button onClick={() => dispatch({type: 'increment'})}>increment</button>
      <button onClick={() => dispatch({type: 'decrement'})}>decrement</button>
    
    </div>
  )
}