前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。本文是一篇以实战为主的文章,主要讲解实际项目中如何使用hooks以及一些最佳实践,不会一步步再介绍一遍react hooks的由来和基本使用,因为写hooks的文章很多,而且官网对于react hooks的介绍也很详细,所以大家不熟悉的可以看一遍官网。

你将收获

  • react hooks核心API使用以及注意事项

  • 实现一个小型redux

  • 实现自定义的useState

  • 实现自定义的useDebounce

  • 实现自定义的useThrottle

  • 实现自定义useTitle

  • 实现自定义的useUpdate

  • 实现自定义的useScroll

  • 实现自定义的useMouse

  • 实现自定义的createBreakpoint

正文

1. react hooks核心API使用注意事项

笔者在项目中常用的hooks主要有useState, useEffect,useCallback,useMemo,useRef。当然像useReducer, useContext, createContext这些钩子在H5游戏中也会使用,因为不需要维护错综复杂的状态,所以我们完全可以由上述三个api构建一个自己的小型redux(后面会介绍如何实现小型的redux)来处理全局状态,但是对于企业复杂项目来说,我们使用redux及其生态会更加高效一些。

我们在使用hooks和函数组件编写我们的组件时,第一个要考虑的就是渲染性能,我们知道如果在不做任何处理时,我们在函数组件中使用setState都会导致组件内部重新渲染,一个比较典型的场景:

当我们在容器组件手动更新了任何state时,容器内部的各个子组件都会重新渲染,为了避免这种情况出现,我们一般都会使用memo将函数组件包裹,来达到class组件的pureComponent的效果:

import React, { memo, useState, useEffect } from 'react'
const A = (props) => {console.log('A1')useEffect(() => {console.log('A2')})return <div>A</div>
}const B = memo((props) => {console.log('B1')useEffect(() => {console.log('B2')})return <div>B</div>
})const Home = (props) => {const [a, setA] = useState(0)useEffect(() => {console.log('start')setA(1)}, [])return <div><A n={a} /><B /></div>
}

当我们将B用memo包裹后,状态a的更新将不会导致B组件重新渲染。其实仅仅优化这一点还远远不够的,比如说我们子组件用到了容器组件的某个变量或者函数,那么当容器内部的state更新之后,这些变量和函数都会重新赋值,这样就会导致即使子组件使用了memo包裹也还是会重新渲染,那么这个时候我们就需要使用useMemo和useCallback了。

useMemo可以帮我们将变量缓存起来,useCallback可以缓存回调函数,它们的第二个参数和useEffect一样,是一个依赖项数组,通过配置依赖项数组来决定是否更新。

import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {const [a, setA] = useState(0)const [b, setB] = useState(0)useEffect(() => {setA(1)}, [])const add = useCallback(() => {console.log('b', b)}, [b])const name = useMemo(() => {return b + 'xuxi'}, [b])return <div><A n={a} /><B add={add} name={name} /></div>
}

此时a更新后B组件不会再重新渲染。以上几个优化步骤主要是用来优化组件的渲染性能,我们平时还会涉及到获取组件dom和使用内部闭包变量的情景,这个时候我们就可以使用useRef。

useRef返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

function AutoFocusIpt() {const inputEl = useRef(null);const useEffect(() => {// `current` 指向已挂载到 DOM 上的文本输入元素inputEl.current.focus();}, []);return (<><input ref={inputEl} type="text" /></>);
}

除了以上应用场景外,我们还可以利用它来实现class组件的setState的功能,具体实现后面会有介绍。

2. 实现一个小型redux

实现redux我们会利用之前说的useReducer, useContext, createContext这三个api,至于如何实现redux,其实网上也有很多实现方式,这里笔者写一个demo供大家参考:

// actionType.js
const actionType = {INSREMENT: 'INSREMENT',DECREMENT: 'DECREMENT',RESET: 'RESET'
}
export default actionType// actions.js
import actionType from './actionType'
const add = (num) => ({type: actionType.INSREMENT,payload: num
})const dec = (num) => ({type: actionType.DECREMENT,payload: num
})const getList = (data) => ({type: actionType.GETLIST,payload: data
})
export {add,dec,getList
}// reducer.js
function init(initialCount) {return {count: initialCount,total: 10,user: {},article: []}
}function reducer(state, action) {switch (action.type) {case actionType.INSREMENT:return {count: state.count + action.payload};case actionType.DECREMENT:return {count: state.count - action.payload};case actionType.RESET:return init(action.payload);default:throw new Error();}
}export { init, reducer }// redux.js
import React, { useReducer, useContext, createContext } from 'react'
import { init, reducer } from './reducer'const Context = createContext()
const Provider = (props) => {const [state, dispatch] = useReducer(reducer, props.initialState || 0, init);return (<Context.Provider value={{state, dispatch}}>{ props.children }</Context.Provider>)
}export { Context, Provider }复制代码

其实还有更优雅的方式实现,笔者之前也写了几套redux模版,欢迎一起讨论哈。接下来我们进入正文,来带大家实现几个常用的自定义hooks。

3. 实现自定义的useState,支持类似class组件setState方法

熟悉react的朋友都知道,我们使用class组件更新状态时,setState会支持两个参数,一个是更新后的state或者回调式更新的state,另一个参数是更新后的回调函数,如下面的用法:

this.setState({num: 1}, () => {console.log('updated')
})

但是hooks函数的useState第二个参数回调支持类似class组件的setState的第一个参数的用法,并不支持第二个参数回调,但是很多业务场景中我们又希望hooks组件能支持更新后的回调这一方法,那该怎么办呢?其实问题也很简单,我们只要对hooks原理和api非常清楚的话,就可以通过自定义hooks来实现,这里我们借助上面提到的useRef和useEffect配合useState来实现这一功能。

注:react hooks的useState一定要放到函数组件的最顶层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致,因为useState底层采用链表结构实现,有严格的顺序之分。

我们先来看看实现的代码:

import { useEffect, useRef, useState } from 'react'const useXState = (initState) => {const [state, setState] = useState(initState)let isUpdate = useRef()const setXState = (state, cb) => {setState(prev => {isUpdate.current = cbreturn typeof state === 'function' ? state(prev) : state})}useEffect(() => {if(isUpdate.current) {isUpdate.current()}})return [state, setXState]}export default useXState

笔者利用useRef的特性来作为标识区分是挂载还是更新,当执行setXstate时,会传入和setState一模一样的参数,并且将回调赋值给useRef的current属性,这样在更新完成时,我们手动调用current即可实现更新后的回调这一功能,是不是很巧妙呢?

4. 实现自定义的useDebounce

节流函数和防抖函数想必大家也不陌生,为了让我们在开发中更优雅的使用节流和防抖函数,我们往往需要让某个state也具有节流防抖的功能,或者某个函数的调用,为了避免频繁调用,我们往往也会采取节截流防抖这一思想,原生的节流防抖函数可能如一下代码所示:

// 节流
function throttle(func, ms) {let previous = 0;return function() {let now = Date.now();let context = this;let args = arguments;if (now - previous > ms) {func.apply(context, args);previous = now;}}
}// 防抖
function debounce(func, ms) {let timeout;return function () {let context = this;let args = arguments;if (timeout) clearTimeout(timeout);timeout = setTimeout(() => {func.apply(context, args)}, ms);}
}

那么我们首先来实现一下防抖的hooks,代码如下:

import { useEffect, useRef } from 'react'const useDebounce = (fn, ms = 30, deps = []) => {let timeout = useRef()useEffect(() => {if (timeout.current) clearTimeout(timeout.current)timeout.current = setTimeout(() => {fn()}, ms)}, deps)const cancel = () => {clearTimeout(timeout.current)timeout = null}return [cancel]}export default useDebounce

由代码可以知道,useDebounce接受三个参数,分别为回调函数,时间间隔以及依赖项数组,它暴露了cancel API,主要是用来控制何时停止防抖函数用的。具体使用如下:

// ...
import { useDebounce } from 'hooks'
const Home = (props) => {const [a, setA] = useState(0)const [b, setB] = useState(0)const [cancel] = useDebounce(() => {setB(a)}, 2000, [a])const changeIpt = (e) => {setA(e.target.value)}return <div><input type="text" onChange={changeIpt} />{ b } { a }</div>
}

以上代码就实现了state的debounce的功能,具体效果如下图所示:

5. 实现自定义的useThrottle

同理,我们继续来实现节流的hooks函数。直接上代码:

import { useEffect, useRef, useState } from 'react'const useThrottle = (fn, ms = 30, deps = []) => {let previous = useRef(0)let [time, setTime] = useState(ms)useEffect(() => {let now = Date.now();if (now - previous.current > time) {fn();previous.current = now;}}, deps)const cancel = () => {setTime(0)}return [cancel]}export default useThrottle

代码和自定义useDebounce类似,但需要注意一点就是为了实现cancel功能,我们使用了内部state来处理,通过控制时间间隔来取消节流效果,当然还有很多其他方法可以实现这个hooks API。具体效果如下:

6. 实现自定义useTitle

自定义的useTitle hooks其实使用场景也很多,因为我们目前大部分项目都是采用SPA或者混合SPA的方式开发,对于不同的路由我们同样希望想多页应用一样能切换到对应的标题,这样可以让用户更好的知道页面的主题和内容。这个hooks的实现也很简单,我们直接上代码:

import { useEffect } from 'react'const useTitle = (title) => {useEffect(() => {document.title = title}, [])return}export default useTitle

以上代码可以看出我们只需要在useEffect中设置document的title属性就好了,我们不需要return任何值。其实还有更优雅和复杂的实现方法,这里就不一一举例了。具体使用如下:

const Home = () => {// ...useTitle('趣谈前端')return <div>home</div>
}

7. 实现自定义的useUpdate

我们都知道如果想让组件重新渲染,我们不得不更新state,但是有时候业务需要的state是没必要更新的,我们不能仅仅为了让组件会重新渲染而强制让一个state做无意义的更新,所以这个时候我们就可以自定义一个更新的hooks来优雅的实现组件的强制更新,实现代码如下:

import { useState } from 'react'const useUpdate = () => {const [, setFlag] = useState()const update = () => {setFlag(Date.now())}return update}export default useUpdate

以上代码可以发现,我们useUpdate钩子返回了一个函数,该函数就是用来强制更新用的。使用方法如下:

const Home = (props) => {// ...const update = useUpdate()return <div>{Date.now()}<div><button onClick={update}>update</button></div></div>
}

效果如下:

8. 实现自定义的useScroll

自定义的useScroll也是高频出现的问题之一,我们往往会监听一个元素滚动位置的变化来决定展现那些内容,这个应用场景在H5游戏开发中应用十分广泛,接下来我们来看看实现代码:

import { useState, useEffect } from 'react'const useScroll = (scrollRef) => {const [pos, setPos] = useState([0,0])useEffect(() => {function handleScroll(e){setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])}scrollRef.current.addEventListener('scroll', handleScroll, false)return () => {scrollRef.current.removeEventListener('scroll', handleScroll, false)}}, [])return pos
}export default useScroll

由以上代码可知,我们在钩子函数里需要传入一个元素的引用,这个我们可以在函数组件中采用ref和useRef来获取到,钩子返回了滚动的x,y值,即滚动的左位移和顶部位移,具体使用如下:

import React, { useRef } from 'react'import { useScroll } from 'hooks'
const Home = (props) => {const scrollRef = useRef(null)const [x, y] = useScroll(scrollRef)return <div><div ref={scrollRef}><div className="innerBox"></div></div><div>{ x }, { y }</div></div>
}

通过使用useScroll,钩子将会帮我们自动监听容器滚动条的变化从而实时获取滚动的位置。具体效果如下:

9. 实现自定义的useMouse和实现自定义的createBreakpoint

自定义的useMouse和createBreakpoint的实现方法和useScroll类似,都是监听窗口或者dom的事件来自动更新我们需要的值,这里我就不一一实现了,如果不懂的可以和我交流。通过这些自定义钩子能大大提高我们代码的开发效率,并将重复代码进行有效复用,所以大家在工作中可以多尝试。

当我们写了很多自定钩子时,一个好的开发经验就是统一管理和分发这些钩子,笔者建议可以在项目中单独建一个hooks的目录专门存放这些可复用的钩子,方便管理和维护。如下:

最后

如果想获取本次项目完整的源码, 或者想学习更多H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。

如果觉得文章还可以,就点个在看支持一下作者吧

10分钟教你手写8个常用的自定义hooks相关推荐

  1. 回调函数自定义传参_10分钟教你手写8个常用的自定义hooks

    作者言写在文章开头的话 Hook 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性.本文是一篇以实战为主的文章,主要讲解实际 ...

  2. 优化自定义函数_10分钟教你手写8个常用的自定义hooks

    前言 Hook 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性.本文是一篇以实战为主的文章,主要讲解实际项目中如何使用ho ...

  3. 手写8个常用的自定义hooks(推荐阅读)

    Hook 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性.本文是一篇以实战为主的文章,主要讲解实际项目中如何使用hooks ...

  4. 20分钟教你手写Sping MVC

    注意:前方高能 彻底理解Spring MVC看这一篇就够了 三层架构 我们的开发架构一般都是基于两种形式,一种是C/S架构,也就是客户端/服务器,另一种是B/S架构,也就 是浏览器服务器.在 Java ...

  5. 手机版python3h如何自制游戏_Python 飞机大战|10 分钟学会用 python 写游戏

    Python 飞机大战|10 分钟学会用 python 写游戏 2018 年 python 语言大火, 这把火看趋势已然延续到了 2019 年! 除了在科学计算领域 python 有用武之地之外, 在 ...

  6. 10分钟教你用python打造贪吃蛇超详细教程

    更多精彩尽在微信公众号[程序猿声] 10分钟教你用python打造贪吃蛇超详细教程 在家闲着没妹子约, 刚好最近又学了一下python,听说pygame挺好玩的.今天就在家研究一下, 弄了个贪吃蛇出来 ...

  7. 10分钟教你用Python玩转微信之抓取好友个性签名制作词云

    10分钟教你用Python玩转微信之抓取好友个性签名制作词云 01 前言+展示 各位小伙伴我又来啦.今天带大家玩点好玩的东西,用Python抓取我们的微信好友个性签名,然后制作词云.怎样,有趣吧~好了 ...

  8. 教你手写DMA传输数据(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.31

    关于DMA与串口原理方面的文章: 嵌入式stm32 复习(工作用)- USART(串口)通信原理知识 2020.3.23 添加链接描述 教你手写串口收发数据(看完这篇你就会手动写啦,保姆级讲解)--- ...

  9. python写好的代码怎么给别人使用-10分钟学会用python写游戏!Python其实很简单!...

    原标题:10分钟学会用python写游戏!Python其实很简单! Python现在非常火,语法简单而且功能强大,很多同学都想学Python!所以在这里给各位看官们准备了高价值Python学习视频教程 ...

最新文章

  1. excel2010设置列宽为像素_怎么改变Excel中列宽的像素
  2. mysql怎么设置密码都不正确的一个解决方法
  3. 电脑开机svchost.exe报错
  4. Hive(五)——查询
  5. struts 2.3.8备忘
  6. 【树上贪心】Tree with Small Distances【codeforces-Round #506-div3-E】
  7. 安装FirefoxSetup,在FirefoxSetup中安装firebug、firepath及简单的xpath使用
  8. Ubuntu配置显卡驱动
  9. 从管理的角度如何提高团队工作效率?
  10. 在Windows系统中安装Go语言
  11. TPM设备管理学习心得
  12. mysql 编程算法_十大编程算法助程序员走上高手之路
  13. 大话 Oracle Grid 云时代的RAC
  14. Javascript学习笔记:call()函数的使用
  15. 二、深入理解运行时数据区
  16. 深耕县域共享单车的大佬松果出行 官网竟是个杂米域名?
  17. 最小二乘、加权最小二乘(WLS)、迭代加权最小二乘(迭代重加全最小二乘)(IRLS)
  18. Python 实现的12306抢票脚本
  19. 软考常考知识点整理-项目进度计划与项目进度管理计划
  20. win8连接wifi成功但受限制_手把手为您win8系统蓝牙经常掉线的操作办法

热门文章

  1. 微信公众号开发快速获取openID(小妙招)
  2. SecureCRT 修改文件夹蓝色字体
  3. 华三指定启动配置文件_06-配置文件管理命令
  4. a.active和a:active的区别
  5. 【win10】微信双开多开最简法
  6. python安装和pip安装diango
  7. AI时代下,人人都在讲开源
  8. 成功的程序化交易系统接口交易员必须具备什么心理条件?
  9. 身份证实名校验接口,身份证二要素核验接口,身份证查询API接口【零存储,数据直连】
  10. 二维码生成、打印工具类