呕心沥血,一文看懂 react hooks

介绍

react hooksReact 16.8 的新增特性。 它可让咱们在函数组件中使用 state 、生命周期以及其余 react 特性,而不只限于 class 组件javascript

react hooks 的出现,标示着 react 中不会在存在无状态组件了,只有类组件和函数组件html

对比

存在即合理,hooks 也不例外,它的出现,就表明了它要解决一些 class 组件的缺陷或者不足,那么咱们先来看看 class 组件有什么不足或者问题存在前端

根据网上的说法我总结出三点,固然每种问题都有其解决方案java

问题 解决方案 缺点
生命周期臃肿、逻辑耦合
逻辑难以复用 经过继承解决 不支持多继承
经过hoc解决 会增长额外的组件嵌套,也会有一些性能影响
渲染属性 同上、层级臃肿、性能影响
class this 指向问题 匿名函数 每次都建立新的函数,子组件重复没必要要渲染
bind 须要写不少跟逻辑、状态无关的代码

hooks 对这些问题都有较好的解决方案react

  1. 没有了 class, 天然就没有了 this 指向问题
  2. 经过自定义 useEffect 来解决复用问题
  3. 经过使用 useEffect 来细分逻辑,减少出现逻辑臃肿的场景

固然,hooks 是一把双刃剑,用的好本身可以达到效果,用的很差反而会 下降开发效率和质量,那么咱们接下来看看如用更好的使用 hooksgit

具体使用

useState 的使用

简单例子

hooks 的能力,就是让咱们在函数组件中使用 state, 就是经过 useState 来实现的,咱们来看一个简单的例子es6

function App () {
    const [ count, setCount ] = useState(0)
    return (
      <div> 点击次数: { count } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
      )
  }
复制代码

useState 的使用很是简单,咱们从 React 中拿到 useState 后,只须要在使用的地方直接调用 useState 函数就能够, useState 会返回一个数组,第一个值是咱们的 state, 第二个值是一个函数,用来修改该 state 的,那么这里为何叫 countsetCount?必定要叫这个吗,这里使用了 es6 的解构赋值,因此你能够给它起任何名字,updateCount, doCountany thing,固然,为了编码规范,因此建议统一使用一种命名规范,尤为是第二个值github

相同值

当咱们在使用 useState 时,修改值时传入一样的值,咱们的组件会从新渲染吗,例如这样web

function App () {
    const [ count, setCount ] = useState(0)
    console.log('component render count')
    return (
      <div> 点击次数: { count } <button onClick={() => { setCount(count)}}>点我</button> </div>
      )
  }
复制代码

结果是不会,放心使用redux

useState 的默认值

useState 支持咱们在调用的时候直接传入一个值,来指定 state 的默认值,好比这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ]),还支持咱们传入一个函数,来经过逻辑计算出默认值,好比这样

function App (props) {
    const [ count, setCount ] = useState(() => {
      return props.count || 0
    })
    return (
      <div> 点击次数: { count } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
      )
  }
复制代码

这个时候,就有小伙伴问了,那我组件每渲染一次,useState 中的函数就会执行一边吗,浪费性能,其实不会,useState 中的函数只会执行一次,咱们能够作个测试

function App (props) {
    const [ count, setCount ] = useState(() => {
      console.log('useState default value function is call')
      return props.count || 0
    })
    return (
      <div> 点击次数: { count } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
      )
  }
复制代码

结果是

setUseState 时获取上一轮的值

咱们在使用 useState 的第二个参数时,咱们想要获取上一轮该 state 的值的话,只须要在 useState 返回的第二个参数,也就是咱们上面的例子中的 setCount 使用时,传入一个参数,该函数的参数就是上一轮的 state 的值

setCount((count => count + 1)

复制代码

多个 useState 的状况

useState 咱们不可能只使用一个,当咱们使用多个 useState 的时候,那 react 是如何识别那个是哪一个呢,其实很简单,它是靠第一次执行的顺序来记录的,就至关于每一个组件存放useState 的地方是一个数组,每使用一个新的 useState,就向数组中 push 一个 useState,那么固然,当咱们在运行时改变、添加、减小 useState 时,react 还能正常执行吗

function App (props) {
  let count, setCount
  let sum, setSum
  if (count > 2) {
    [ count, setCount ] = useState(0)
    [ sum, setSum ] = useState(10)
  } else {
    [ sum, setSum ] = useState(10)
    [ count, setCount ] = useState(0)
  }
  return (
    <div> 点击次数: { count } 总计:{ sum } <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button> </div>
    )
}
复制代码

当咱们在运行时改变 useState 的顺序,数据会混乱,增长 useState, 程序会报错

不要在循环,条件或嵌套函数中调用 Hook, 确保老是在你的 React 函数的最顶层调用他们。遵照这条规则,你就能确保 Hook 在每一次渲染中都按照一样的顺序被调用。这让 React 可以在屡次的 useStateuseEffect 调用之间保持 hook 状态的正确

同时推荐使用 eslint-plugin-react-hooks 插件来规范代码编写,针对这种状况进行校验

useState 的使用就是这么简单,我已经学会了, 接下来,咱们看一下 useEffect 的使用

useEffect 的使用

Effect Hook 可让你在函数组件中执行反作用操做,这里提到反作用,什么是反作用呢,就是除了状态相关的逻辑,好比网络请求,监听事件,查找 dom

简单例子

有这样一个需求,须要咱们在组件在状态更新的时候改变 document.title,在之前咱们会这样写代码

class App extends PureComponent {
    state = {
      count: 0
    }

    componentDidMount() {
      document.title = count
    }

    componentDidUpdate() {
      document.title = count
    }
    render () {
      const { count } = this.state
      return (
        <div> 页面名称: { count } <button onClick={() => { this.setState({ count: count++ })}}>点我</button> </div>
      )
    }
  }
复制代码

使用 hooks 怎么写呢

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

  useEffect(() => {
    document.title = count
  })

  return (
    <div> 页面名称: { count } <button onClick={() => { setCount(count + 1 )}}>点我</button> </div>
    )
}
复制代码

useEffect 是什么呢,咱们先忽略,回到咱们总结的 class 组件存在的问题,useState 只是让咱们的函数组件具备使用 state 的能力,那咱们要解决 class 组件存在的问题,先来解决第一个,生命周期臃肿的问题

useEffect 生命周期

若是你熟悉 React class 的生命周期函数,你能够把 useEffect Hook 看作 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

以往咱们在绑定事件、解绑事件、设定定时器、查找 dom 的时候,都是经过 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期来实现的,而 useEffect 会在组件每次 render 以后调用,就至关于这三个生命周期函数,只不过能够经过传参来决定是否调用

其中注意的是,useEffect 会返回一个回调函数,做用于清除上一次反作用遗留下来的状态,若是该 useEffect 只调用一次,该回调函数至关于 componentWillUnmount 生命周期

具体看下面例子

function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      window.addEventListener('resize', onChange, false)

      return () => {
        window.removeEventListener('resize', onChange, false)
      }
    })

    useEffect(() => {
      document.title = count
    })

    return (
      <div> 页面名称: { count } 页面宽度: { width } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
      )
  }
复制代码

接着咱们前面的简单例子,咱们上面例子要处理两种反作用逻辑,这里咱们既要处理 title,还要监听屏幕宽度改变,按照 class 的写法,咱们要在生命周期中处理这两种逻辑,但在 hooks 中,咱们只须要两个 useEffect 就能解决这些问题,咱们以前提到,useEffect 可以返回一个函数,用来清除上一次反作用留下的状态,这个地方咱们能够用来解绑事件监听,这个地方存在一个问题,就是 useEffect 是每次 render 以后就会调用,好比 title 的改变,至关于 componentDidUpdate,但咱们的事件监听不该该每次 render 以后,进行一次绑定和解绑,就是咱们须要 useEffect 变成 componentDidMount, 它的返回函数变成 componentWillUnmount,这里就须要用到 useEffect 函数的第二个参数了

useEffect 的第二个参数

useEffect 的第二个参数,有三种状况

  1. 什么都不传,组件每次 render 以后 useEffect 都会调用,至关于 componentDidMountcomponentDidUpdate
  2. 传入一个空数组 [], 只会调用一次,至关于 componentDidMountcomponentWillUnmount
  3. 传入一个数组,其中包括变量,只有这些变量变更时,useEffect 才会执行

具体看下面例子

function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      // 至关于 componentDidMount
      console.log('add resize event')
      window.addEventListener('resize', onChange, false)

      return () => {
        // 至关于 componentWillUnmount
        window.removeEventListener('resize', onChange, false)
      }
    }, [])

    useEffect(() => {
      // 至关于 componentDidUpdate
      document.title = count
    })

    useEffect(() => {
      console.log(`count change: count is ${count}`)
    }, [ count ])

    return (
      <div> 页面名称: { count } 页面宽度: { width } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
      )
  }
复制代码

根据上面的例子的运行结果,第一个 useEffect 中的 'add resize event' 只会在第一次运行时输出一次,不管组件怎么 render,都不会在输出,第二个 useEffect 会在每次组件 render 以后都执行,title 每次点击都会改变, 第三个 useEffect, 只要有在第一次运行和 count 改变时,才会执行,屏幕发生改变引发的 render 并不会影响第三个 useEffect

useContext

关于 react 中如何使用 context,这里就不细说,能够看我以前写的 React 中 Context 的使用

context 中的 ProviderConsumer,在类组件和函数组件中都能使用,contextType 只能在类组件中使用,由于它是类的静态属性,具体如何使用 useContext 呢,看下面的例子

// 建立一个 context
const Context = createContext(0)

// 组件一, Consumer 写法
class Item1 extends PureComponent {
  render () {
    return (
      <Context.Consumer>
        {
          (count) => (<div>{count}</div>)
        }
      </Context.Consumer>
    )
  }
}
// 组件二, contextType 写法
class Item2 extends PureComponent {
  static contextType = Context
  render () {
    const count = this.context
    return (
      <div>{count}</div>
    )
  }
}
// 组件一, useContext 写法
function Item3 () {
  const count = useContext(Context);
  return (
    <div>{ count }</div>
  )
}

function App () {
  const [ count, setCount ] = useState(0)
  return (
    <div>
      点击次数: { count } 
      <button onClick={() => { setCount(count + 1)}}>点我</button>
      <Context.Provider value={count}>
        <Item1></Item1>
        <Item2></Item2>
        <Item3></Item3>
      </Context.Provider>
    </div>
    )
}
复制代码

经过运行上面的例子,咱们获得的结果是,三种写法都可以实现咱们的需求,可是,三种写有各自的优缺点,下面为对比出的结果

写法 优缺点
consumer 嵌套复杂,Consumer 第一个子节点必须为一个函数,无形增长了工做量
contextType 只支持 类组件,没法在多 context 的状况下使用
useContext 不须要嵌套,多 context 写法简单

经过上面的比较,没理由继续使用 consumercontextType

useMemo

useMemo 是什么呢,它跟 memo 有关系吗, memo 的具体内容能够查看 React 中性能优化、 memo、PureComponent、shouldComponentUpdate 的使用,说白了 memo 就是函数组件的 PureComponent,用来作性能优化的手段,useMemo 也是,useMemo 在个人印象中和 Vuecomputed 计算属性相似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变,useMemo 具体如何使用呢,看下面例子

function App () {
  const [ count, setCount ] = useState(0)
  const add = useMemo(() => {
    return count + 1
  }, [count])
  return (
    <div> 点击次数: { count } <br/> 次数加一: { add } <button onClick={() => { setCount(count + 1)}}>点我</button> </div>
    )
}
复制代码

上面的例子中,useMemo 也支持传入第二个参数,用法和 useEffect 相似

  1. 不传数组,每次更新都会从新计算
  2. 空数组,只会计算一次
  3. 依赖对应的值,当对应的值发生变化时,才会从新计算(能够依赖另一个 useMemo 返回的值)

须要注意的是,useMemo 会在渲染的时候执行,而不是渲染以后执行,这一点和 useEffect 有区别,因此 useMemo 不建议有 反作用相关的逻辑

同时,useMemo 能够做为性能优化的手段,但不要把它当成语义上的保证,未来,React 可能会选择“遗忘”之前的一些 memoized 值,并在下次渲染时从新计算它们

useCallback

useCallback 是什么呢,能够说是 useMemo 的语法糖,能用 useCallback 实现的,均可以使用 useMemo, 在 react 中咱们常常面临一个子组件渲染优化的问题,细节能够查看React 中性能优化、 memo、PureComponent、shouldComponentUpdate 的使用,尤为是在向子组件传递函数props时,每次 render 都会建立新函数,致使子组件没必要要的渲染,浪费性能,这个时候,就是 useCallback 的用武之地了,useCallback 能够保证,不管 render 多少次,咱们的函数都是同一个函数,减少不断建立的开销,具体如何使用看下面例子

const onClick = `useMemo`(() => {
  return () => {
    console.log('button click')
  }
}, [])

const onClick = useCallback(() => {
 console.log('button click')
}, [])
复制代码

一样,useCallback 的第二个参数和useMemo同样,没有区别

useRef

useRef 有什么做用呢,其实很简单,总共有两种用法

  1. 获取子组件的实例(只有类组件可用)
  2. 在函数组件中的一个全局变量,不会由于重复 render 重复申明, 相似于类组件的 this.xxx

获取子组件实例

上面提到了一点,useRef 只能获取子组件的实例,这在类组件中也是一样的道理,具体看下面的例子

// 使用 ref 子组件必须是类组件
class Children extends PureComponent {
  render () {
    const { count } = this.props
    return (
      <div>{ count }</div>
    )
  }
}

function App () {
  const [ count, setCount ] = useState(0)
  const childrenRef = useRef(null)
  // const 
  const onClick = useMemo(() => {
    return () => {
      console.log('button click')
      console.log(childrenRef.current)
      setCount((count) => count + 1)
    }
  }, [])
  return (
    <div> 点击次数: { count } <Children ref={childrenRef} count={count}></Children> <button onClick={onClick}>点我</button> </div>
    )
}
复制代码

useRef 在使用的时候,能够传入默认值来指定默认值,须要使用的时候,访问 ref.current 便可访问到组件实例

类组件属性

有些状况下,咱们须要保证函数组件每次 render 以后,某些变量不会被重复申明,好比说 Dom 节点,定时器的 id 等等,在类组件中,咱们彻底能够经过给类添加一个自定义属性来保留,好比说 this.xxx, 可是函数组件没有 this,天然没法经过这种方法使用,有的朋友说,我可使用 useState 来保留变量的值,可是 useState 会触发组件 render,在这里彻底是不须要的,咱们就须要使用 useRef 来实现了,具体看下面例子

function App () {
  const [ count, setCount ] = useState(0)
  const timer = useRef(null)
  let timer2 
  
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + 1)
    }, 500)

    timer.current = id
    timer2 = id
    return () => {
      clearInterval(timer.current)
    }
  }, [])

  const onClickRef = useCallback(() => {
    clearInterval(timer.current)
  }, [])

  const onClick = useCallback(() => {
    clearInterval(timer2)
  }, [])

  return (
    <div> 点击次数: { count } <button onClick={onClick}>普通</button> <button onClick={onClickRef}>useRef</button> </div>
    )
}
复制代码

当咱们们使用普通的按钮去暂停定时器时发现定时器没法清除,由于 App 组件每次 render,都会从新申明一次 timer2, 定时器的 id 在第二次 render 时,就丢失了,因此没法清除定时器,针对这种状况,就须要使用到 useRef,来为咱们保留定时器 id,相似于 this.xxx,这就是 useRef 的另一种用法

useReducer

useReducer 是什么呢,它其实就是相似 redux 中的功能,相较于 useState,它更适合一些逻辑较复杂且包含多个子值,或者下一个 state 依赖于以前的 state 等等的特定场景, useReducer 总共有三个参数

  1. 第一个参数是 一个 reducer,就是一个函数相似 (state, action) => newState 的函数,传入 上一个 state 和本次的 action
  2. 第二个参数是初始 state,也就是默认值,是比较简单的方法
  3. 第三个参数是惰性初始化,这么作能够将用于计算 state 的逻辑提取到 reducer 外部,这也为未来对重置 stateaction 作处理提供了便利

具体使用方法看下面的例子

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0
  });
  return (
    <> 点击次数: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 复制代码

useImperativeHandle

useImperativeHandle 可让你在使用 ref 时自定义暴露给父组件的实例值,说简单点就是,子组件能够选择性的暴露给副组件一些方法,这样能够隐藏一些私有方法和属性,官方建议,useImperativeHandle应当与 forwardRef 一块儿使用,具体如何使用看下面例子

function Kun (props, ref) {
  const kun = useRef()

  const introduce = useCallback (() => {
    console.log('i can sing, jump, rap, play basketball')
  }, [])
  useImperativeHandle(ref, () => ({
    introduce: () => {
      introduce()
    }
  }));

  return (
    <div ref={kun}> { props.count }</div>
  )
}

const KunKun = forwardRef(Kun)

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

  const onClick = useCallback (() => {
    setCount(count => count + 1)
    kunRef.current.introduce()
  }, [])
  return (
    <div> 点击次数: { count } <KunKun ref={kunRef} count={count}></KunKun> <button onClick={onClick}>点我</button> </div>
    )
}
复制代码

其它hook

还有两个 hook,没什么好讲的,用的也很少,能够看看官方文档

  1. useLayoutEffect
  2. useDebugValue

自定义hook

咱们以前总结出三个问题,class this 指向问题,生命周期逻辑冗余问题,都已获得解决,而逻辑难以复用,在前面的例子中并无解决,要解决这个问题,就要经过 自定义 hook 来解决

自定义 Hook,能够将组件逻辑提取到可重用的函数中,来解决逻辑难以复用问题

前面有个例子是获取屏幕宽度变化的例子,假设咱们有诸多组件都须要这个逻辑,那么咱们只须要将其抽取成一个自定义 hook 便可,具体实现看下面例子

function useWidth (defaultWidth) {
  const [width, setWidth] = useState(document.body.clientWidth)

  const onChange = useCallback (() => {
    setWidth(document.body.clientWidth)
  }, [])

  useEffect(() => {
    window.addEventListener('resize', onChange, false)

    return () => {
      window.removeEventListener('resize', onChange, false)
    }
  }, [onChange])

  return width
}

function App () {

  const width = useWidth(document.body.clientWidth)

  return (
    <div> 页面宽度: { width } </div>
    )
}
复制代码

经过上面的例子,咱们能够看出

自定义 hook 是一个函数,其名称以 use 开头,函数内部能够调用其余的 hook,至于为何要以 use 开头,是由于若是不以 use 开头,React 就没法判断某个函数是否包含对其内部 hook 的调用,React 也将没法自动检查你的 hook 是否违反了 hook 的规则,因此要以 use 开头

自定义 hook,真的很简单,不过具体什么样的逻辑,须要抽离成自定义 hook,这就须要工做中不段积累的经验去判断,避免为了 hookhook

总结

在我学习和使用自定义 hook 时,我发现其实它的道理很简单,不少前端框架、库里面都有相似的概念,框架和库的设计最后都疏通同归,因此咱们在学习一个新的框架、库或者理念时,不该该将其是为一个全新的东西,而更多的应该从自身掌握的内容去推导,去举一反三,这样咱们在学习的时候会事半功倍,在日益更新的前端领域,可以抽出更多的时间去理解更为核心的内容

最后,若是本文对你有任何帮助的话,感谢点个赞 💗

参考

  1. react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-…
  2. reactjs.org/docs/hooks-…

react 其余文章

  1. React 中 lazy, Suspense 以及错误边界(Error Boundaries)的使用
  2. React 中 Context 的使用
  3. React 中性能优化、 memo、 PureComponent、shouldComponentUpdate 的使用
相关文章
相关标签/搜索