优化自定义函数_玩转reacthooks,自定义hooks设计模式及其实战
前言
自从react16.8,react-hooks
诞生以来,在工作中一直使用hooks
,一年多的时间里,接触的react项目,渐渐使用function
无状态组件代替了classs
声明的有状态组件,期间也总结了一些心得。尤其对于近期三个月的项目里,一点点用自定义hooks来处理公司项目中重复逻辑,总体感觉还不错。今天给大家讲讲我在工作中对react-hooks
心得,和一些自定义hooks的设计思想,把在工作中的经验分享给大家。
自定义hooks设计
又回到那个问题?什么是hooks。
react-hooks
是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks
思想和初衷,也是把组件,颗粒化,单元化,形成独立的渲染环境,减少渲染次数,优化性能。
还不明白react-hooks的伙伴可以看的另外一篇文章: react-hooks如何使用?
什么是自定义hooks
自定义hooks是在react-hooks
基础上的一个拓展,可以根据业务需要制定满足业务需要的hooks,更注重的是逻辑单元。通过业务场景不同,我们到底需要react-hooks
做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义hooks产生的初衷。
如何设计一个自定义hooks,设计规范
逻辑+ 组件
hooks 专注的就是逻辑复用, 是我们的项目,不仅仅停留在组件复用的层面上。hooks让我们可以将一段通用的逻辑存封起来。将我们需要它的时候,开箱即用即可。
自定义hooks-驱动条件
hooks
本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks
的执行,由此可见组件本身执行和hooks的执行如出一辙。
那么prop
的修改,useState,useReducer
使用是无状态组件更新条件,那么就是驱动hooks执行的条件。我们用一幅图来表示如上关系。
自定义hooks-通用模式
我们设计的自定义react-hooks
应该是长的这样的。
const [ xxx , ... ] = useXXX(参数A,参数B...)
在我们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么,返回什么。返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。正如下图所示
自定义hooks-条件限定
如果自定义hooks没有设计好,比如返回一个改变state的函数,但是没有加条件限定限定,就有可能造成不必要的上下文的执行,更有甚的是组件的循环渲染执行。
比如:我们写一个非常简单hooks来格式化数组将小写转成大写。
import React , { useState } from 'react'/* 自定义hooks 用于格式化数组将小写转成大写 */function useFormatList(list){return list.map(item=>{console.log(1111)return item.toUpperCase() })}/* 父组件传过来的list = [ 'aaa' , 'bbb' , 'ccc' ] */function index({ list }){const [ number ,setNumber ] = useState(0)const newList = useFormatList(list)return <div><div className="list" > { newList.map(item=><div key={item} >{ item }div>) }div><div className="number" ><div>{ number }div><button onClick={()=> setNumber(number + 1) } >addbutton>div>div>}export default index
如上述问题,我们格式化父组件传递过来的list
数组,并将小写变成大写,但是当我们点击add
。理想状态下数组不需要重新format
,但是实际跟着执行format
。无疑增加了性能开销。
所以我们在设置自定义hooks的时候,一定要把条件限定-性能开销加进去。
于是乎我们这样处理一下。
function useFormatList(list) {return useMemo(() => list.map(item => {console.log(1111)return item.toUpperCase() }), [])}
华丽丽的解决了如上的问题。
所以一个好用的自定义hooks,一定要配合useMemo ,useCallback
等api
一起使用。
自定义hooks实战
准备工作:搭建demo样式项目
为了将实际的业务情景和自定义hooks
连接在一起,我这里用 taro-h5
构建了一个移动端react
项目。用于描述实际工作中用到自定义hooks
的场景。
demo项目地址 : 自定义hooks,demo项目
后续会更新更多自定义hooks,或者感兴趣的同学可以关注一下这个项目,或者也可以一起维护这个项目。
项目结构
page
文件夹里包括自定义hooks展示demo
页面,hooks
文件夹里面是自定义hooks内容。
展示效果
每个listItem
记录每一个完成自定义hooks展示效果,陆续还有其他的hooks
。我们接下来看看hooks具体实现。
实战一:控制滚动条-吸顶效果,渐变效果-useScroll
背景:公司的一个h5项目,在滚动条滚动的过程中,需要控制 渐变 + 高度 + 吸顶效果。
1实现效果
1 首先红色色块有吸顶效果。2 粉色色块,是固定上边但是有少量偏移,加上逐渐变透明效果。
2 自定义useScroll
设计思路
需要实现功能:
1 监听滚动条滚动。2 计算吸顶临界值,渐变值,透明度。3 改变state
渲染视图。
好吧,接下来让我们用一个hooks
来实现上述工作。
页面
import React from 'react'import { View, Swiper, SwiperItem } from '@tarojs/components'import useScroll from '../../hooks/useScroll'import './index.less'export default function Index() { const [scrollOptions,domRef] = useScroll()/* scrollOptions 保存控制透明度 ,top值 ,吸顶开关等变量 */const { opacity, top, suctionTop } = scrollOptionsreturn <View style={{ position: 'static', height: '2000px' }} ><View className='white' /><View id='box' style={{ opacity, transform: `translateY(${top}px)` }} ><SwiperclassName='swiper' ><SwiperItem className='SwiperItem' ><View className='imgae' />SwiperItem>Swiper>View><View className={suctionTop ? 'box_card suctionTop' : 'box_card'}><Viewstyle={{background: 'red',boxShadow: '0px 15px 10px -16px #F02F0F' }}className='reultCard' >View>View>View>}
我们通过一个scrollOptions
来保存透明度 ,top
值 ,吸顶开关等变量,然后通过返回一个ref
作为dom
元素的采集器。接下来就是hooks如果实现的。
useScroll
export default function useScroll() {const dom = useRef(null)const [scrollOptions, setScrollOptions] = useState({top: 0,suctionTop: false,opacity: 1 })
useEffect(() => {const box = (dom.current)const offsetHeight = box.offsetHeightconst radio = box.offsetHeight / 500 * 20const handerScroll = () => {const scrollY = window.scrollY/* 控制透明度 */const computerOpacty = 1 - scrollY / 160/* 控制吸顶效果 */const offsetTop = offsetHeight - scrollY - offsetHeight / 500 * 84const top = 0 - scrollY / 5 setScrollOptions({opacity: computerOpacty <= 0 ? 0 : computerOpacty, top,suctionTop: offsetTop < radio }) }document.addEventListener('scroll', handerScroll)return function () {document.removeEventListener('scroll', handerScroll) } }, [])return [scrollOptions, dom]}
具体设计思路
1 我们用一个 useRef
来获取需要元素 2 用 useEffect
来初始化绑定/解绑事件 3 用 useState
来保存要改变的状态,通知组件渲染。
中间的计算过程我们可以先不计,最终达到预期效果。
有关性能优化
这里说一下一个无关hooks本身的性能优化点,我们在改变top
值的时候 ,尽量用改变transform
Y值代替直接改变top值,原因如下
1 transform
是可以让GPU加速的CSS3
属性,在性能方便优于直接改变top
值。2 在ios
端,固定定位频繁改变top
值,会出现闪屏兼容性。
实战二:控制表单状态-useFormChange
背景:但我们遇到例如 列表的表头搜索,表单提交等场景,需要逐一改变每个formItem
的value
值,需要逐一绑定事件是比较麻烦的一件事,于是在平时的开发中,我们来用一个hooks来统一管理表单的状态。
1 实现效果
demo效果如下
获取表单
重置表单
2 自定义useFormChange
设计思路
需要实现功能
1 控制每一个表单的值。2 具有表单提交,获取整个表单数据功能。3 点击重置,重置表单功能。
页面
import useFormChange from '../../hooks/useFormChange'import './index.less'const selector = ['嘿嘿', '哈哈', '嘻嘻']function index() {const [formData, setFormItem, reset] = useFormChange()const { name, options, select } = formDatareturn <View className='formbox' ><View className='des' >文本框View><AtInput name='value1' title='名称' type='text' placeholder='请输入名称' value={name} onChange={(value) => setFormItem('name', value)} /><View className='des' >单选View><AtRadiooptions={[ { label: '单选项一', value: 'option1' }, { label: '单选项二', value: 'option2' }, ]}value={options}onClick={(value) => setFormItem('options', value)} /><View className='des' >下拉框View><Picker mode='selector' range={selector} onChange={(e) => setFormItem('select',selector[e.detail.value])} ><AtList><AtListItemtitle='当前选择'extraText={select} />AtList>Picker><View className='btns' ><AtButton type='primary' onClick={() => console.log(formData)} >提交AtButton><AtButton className='reset' onClick={reset} >重置AtButton>View>View>}
useFormChange
/* 表单/表头搜素hooks */function useFormChange() {const formData = useRef({})const [, forceUpdate] = useState(null)const handerForm = useMemo(()=>{/* 改变表单单元项 */const setFormItem = (keys, value) => { const form = formData.current form[keys] = value forceUpdate(value) }/* 重置表单 */const resetForm = () => {const current = formData.currentfor (let name in current) { current[name] = '' } forceUpdate('') }return [ setFormItem ,resetForm ] },[])
return [ formData.current ,...handerForm ] }
具体流程分析:1 我们用useRef
来缓存整个表单的数据。2 用useState
单独做更新,不需要读取useState状态。3 声明重置表单方法resetForm
, 设置表单单元项change
方法,
这里值得一提的问题是 为什么用useRef
来缓存formData
数据,而不是直接用useState
。
原因一 我们都知道当用useMemo,useCallback
等API
的时候,如果引用了useState
,就要把useState值作为deps传入,否侧由于useMemo,useCallback
缓存了useState
旧的值,无法得到新得值,但是useRef
不同,可以直接读取/改变useRef
里面缓存的数据。
原因二 同步useState
useState
在一次使用useState
改变state
值之后,我们是无法获取最新的state
,如下demo
function index(){const [ number , setNumber ] = useState(0)const changeState = ()=>{ setNumber(number+1)console.log(number) //组件更新 -> 打印number为0 -> 并没有获取到最新的值 }return <View><Button onClick={changeState} >点击改变stateButton>View>}
我们可以用 useRef
和 useState
达到同步效果
function index(){const number = useRef(0)const [ , forceUpdate ] = useState(0)const changeState = ()=>{ number.current++ forceUpdate(number.current)console.log(number.current) //打印值为1,组件更新,值改变 }return <View><Button onClick={changeState} >点击改变stateButton>View>}
性能优化 用useMemo
来优化setFormItem ,resetForm
方法,避免重复声明,带来的性能开销。
实战三:控制表格/列表-useTableRequset
背景:当我们需要控制带分页,带查询条件的表格/列表的情况下。
1 实现效果
1 统一管理表格的数据,包括列表,页码,总页码数等信息 2 实现切换页码,更新数据。
2 自定义useTableRequset
设计思路
1 我们需要state
来保存列表数据,总页码数,当前页面等信息。2 需要暴露一个方法用于,改变分页数据,从新请求数据。
解析来我们看一下具体的实现方案。
页面
function getList(payload){const query = formateQuery(payload)return fetch('http://127.0.0.1:7001/page/tag/list?'+ query ).then(res => res.json())}export default function index(){/* 控制表格查询条件 */const [ query , setQuery ] = useState({})const [tableData, handerChange] = useTableRequest(query,getList)const { page ,pageSize,totalCount ,list } = tableDatareturn <View className='index' ><View className='table' ><View className='table_head' ><View className='col' >技术名称View><View className='col' >iconView><View className='col' >创建时间View>View><View className='table_body' > { list.map(item=><View className='table_row' key={item.id} ><View className='col' >{ item.name }View><View className='col' > <Image className='col col_image' src={Icons[item.icon].default} />View><View className='col' >{ item.createdAt.slice(0,10) }View>View>) }View>View><AtPagination total={Number(totalCount)} iconpageSize={Number(pageSize)}onPageChange={(mes)=>handerChange({ page:mes.current })} current={Number(page)} >AtPagination>View>}
useTableRequset
/* table 数据更新 hooks */export default function useTableRequset(query, api) {/* 是否是第一次请求 */const fisrtRequest = useRef(false)/* 保存分页信息 */const [pageOptions, setPageOptions] = useState({page: 1,pageSize: 3 })/* 保存表格数据 */const [tableData, setTableData] = useState({list: [],totalCount: 0,pageSize: 3,page:1, })/* 请求数据 ,数据处理逻辑根后端协调着来 */const getList = useMemo(() => {return async payload => {if (!api) returnconst data = await api(payload || {...query, ...pageOptions})if (data.code == 0) { setTableData(data.data) fisrtRequest.current = true } } }, [])/* 改变分页,重新请求数据 */ useEffect(() => { fisrtRequest.current && getList({ ...query, ...pageOptions }) }, [pageOptions])/* 改变查询条件。重新请求数据 */ useEffect(() => { getList({ ...query, ...pageOptions,page: 1 }) }, [query])/* 处理分页逻辑 */const handerChange = useMemo(() => (options) => setPageOptions({...options }), [])
return [tableData, handerChange, getList] }
具体设计思路:
因为是demo
项目,我们用本地服务器做了一个数据查询的接口,为的是模拟数据请求。
1 用一个useRef
来缓存是否是第一次请求数据。
2 用useState
保存返回的数据和分页信息。
3 用两个useEffect
分别处理,对于列表查询条件的更改,或者是分页状态更改,启动副作用钩子,重新请求数据,这里为了区别两种状态更改效果,实际也可以用一个effect
来处理。
4 暴露两个方法,分别是请求数据和处理分页逻辑。
性能优化
1 我们用一个useRef
来缓存是否是第一次渲染,目的是为了,初始化的时候,两个useEffect
钩子都会执行,为了避免重复请求数据。
2 对于请求数据和处理分页逻辑,避免重复声明,我们用useMemo
加以优化。
需要注意的是,这里把请求数据后处理逻辑连同自定义hooks封装在一起,在实际项目中,要看和后端约定的数据返回格式来制定属于自己的hooks。
实战四:控制拖拽效果-useDrapDrop
背景:用transform
和hooks
实现了拖拽效果,无需设置定位。
1 实现效果
独立hooks
绑定独立的dom
元素,使之能实现自由拖拽效果。
2 useDrapDrop
具体实现思路
需要实现的功能:
1 通过自定义hooks
计算出来的 x ,y 值,通过将transform
的translate
属性设置当前计算出来的x,y
实现拖拽效果。
2 自定义hooks
能抓取当前dom
元素容器。
页面
export default function index (){const [ style1 , dropRef ]= useDrapDrop()const [style2,dropRef2] = useDrapDrop()return <View className='index'><View className='drop1' ref={dropRef}style={{transform:`translate(${style1.x}px, ${style1.y}px)`}} >drop1View><View className='drop2' ref={dropRef2}style={{transform:`translate(${style2.x}px, ${style2.y}px)`}} >drop2View><View className='drop3' >drop3View>View>}
注意点: 我们没有用,left
,和top
来改变定位,css3
的transform
能够避免浏览器的重排和回流,性能优化上要强于直接改变定位的top,left值。由于我们模拟环境考虑到是h5移动端,所以用 webview
的 touchstart , touchmove ,ontouchend
事件来进行模拟。
核心代码-useDrapDrop
/* 移动端 -> 拖拽自定义效果(不使用定位) */function useDrapDrop() {/* 保存上次移动位置 */ const lastOffset = useRef({x:0, /* 当前x 值 */y:0, /* 当前y 值 */X:0, /* 上一次保存X值 */Y:0, /* 上一次保存Y值 */ }) /* 获取当前的元素实例 */const currentDom = useRef(null)/* 更新位置 */const [, foceUpdate] = useState({})/* 监听开始/移动事件 */const [ ontouchstart ,ontouchmove ,ontouchend ] = useMemo(()=>{/* 保存left right信息 */const currentOffset = {} /* 开始滑动 */const touchstart = function (e) { const targetTouche = e.targetTouches[0] currentOffset.X = targetTouche.clientX currentOffset.Y = targetTouche.clientY }/* 滑动中 */const touchmove = function (e){const targetT = e.targetTouches[0]let x =lastOffset.current.X + targetT.clientX - currentOffset.Xlet y =lastOffset.current.Y + targetT.clientY - currentOffset.Y lastOffset.current.x = x lastOffset.current.y = y foceUpdate({ x,y }) }/* 监听滑动停止事件 */const touchend = () => { lastOffset.current.X = lastOffset.current.x lastOffset.current.Y = lastOffset.current.y }return [ touchstart , touchmove ,touchend] },[]) useLayoutEffect(()=>{const dom = currentDom.current dom.ontouchstart = ontouchstart dom.ontouchmove = ontouchmove dom.ontouchend = ontouchend },[])return [ { x:lastOffset.current.x,y:lastOffset.current.y } , currentDom]}
具体设计思路:
1 对于拖拽效果,我们需要实时获取dom
元素的位置信息,所以我们需要一个useRef
来抓取dom元素。
2 由于我们用的是transfrom
改变位置,所以需要保存一下当前位置和上一次transform
的位置,所以我们用一个useRef来缓存位置。
3 我们通过useRef
改变x,y
值,但是需要渲染新的位置,所以我们用一个useState
来专门产生组件更新。
4 初始化的时候我们需要给当前的元素绑定事件,因为在初始化的时候我们可能精确需要元素的位置信息,所以我们用useLayoutEffect
钩子来绑定touchstart , touchmove ,ontouchend
等事件。
总结
以上就是我在react自定义hooks上的总结,和一些实际的应用场景,我们项目中,80%的表单列表场景,都可以用上述hooks来解决。
纸上得来终觉浅,绝知此事要躬行,真正玩好,玩转hooks,是一个日积月累的过程,怎么去设计一个符合业务场景的hooks,需要我们不断的实战,不断的总结。
关注我们
公众号ID:前端Sharing
天冷记得加衣哦
优化自定义函数_玩转reacthooks,自定义hooks设计模式及其实战相关推荐
- Spark SQL自定义函数_第五章
1.自定义函数分类 类似于hive当中的自定义函数, spark同样可以使用自定义函数来实现新的功能. spark中的自定义函数有如下3类 1.UDF(User-Defined-Function) 输 ...
- 自定义函数_自定义函数,让你的表格为所欲为
自定义函数可以做什么?可以让你的表格为所欲为! 这篇文章教你如何掌握自定义函数. 函数是大家在使用 Excel 工作的过程中经常会用到的. 比如大家已经很熟悉了的求和函数 SUM,计数函数 COUNT ...
- mysql自定义函数的分号_MySQL之自定义函数实例讲解
转自:https://www.2cto.com/database/201804/740205.html MySQL中已经有很多函数,如时间函数等,但是有时这些函数不能满足自己的设计需求,此时需要自定义 ...
- R语言构建决策树模型(decision tree)并可视化决策树:自定义函数计算对数似然、自定义函数计算模型的分类效能(accuray、F1、偏差Deviance)、使用pander包美化界面输出内容
R语言构建决策树模型(decision tree)并可视化决策树:自定义函数计算对数似然.自定义函数计算模型的分类效能(accuray.F1.偏差Deviance).使用pander包美化界面输出内容 ...
- python如何自定义函数_python如何自定义函数_后端开发
c语言特点是什么_后端开发 c语言特点是:1.语言简洁.紧凑,使用方便.灵活:2.运算符丰富:3.数据结构丰富,具有现代化语言的各种数据结构:4.具有结构化的控制语句:5.语法限制不太严度格,程序设计 ...
- 自定义函数_Access中的WorkDate自定义函数
Access专家课堂两周年庆,视频课程.培训班.企业版等5~8折优惠,->点此了解详情 在Access中使用类似EXCEL中的WorkDate函数. 在Excel中有workdate函数,在AC ...
- mysql存储过程和自定义函数_MySQL存储过程/存储过程与自定义函数的区别
语法: 创建存储过程: CREATE[definer = {user|current_user}] PROCEDURE sp_name ([ proc_parameter [,proc_paramet ...
- matlab自定义函数如何报错,MATLAB自定义函数
MATLAB自定义函数形式 function [a,b,c] = funname(x1,x2,x3) 输入变量 对于输入变量,MATLAB可以识别输入变量的个数,通过nargin来记录当前输入变量个数 ...
- 最小值c语言编写自定义函数,C语言笔记55:自定义函数[老九学堂]
函数定义 return_type function_name ([datetype1 arg1],[datype2 arg2,[...]){ //函数体 } 函数三要素返回值类型 函数名 参数列表 书 ...
最新文章
- 项目管理流程有哪些?如何才能让项目管理更有效?
- 深入浅出Spring Security(二):FilterChainProxy的创建过程
- 开课吧python课程-开课吧的python课程怎么样,值得报名吗?
- 开发相关手册、STM32各种库文件、相关软件、工具连接等(不断更新)
- 苹果手机java_iphone手机,苹果手机如何登陆网易163邮箱
- 紫书 程序 3-3 蛇形填数
- c语言n次方怎么输入_C语言实现斐波拉契数列
- 怎样洗头使头发变黑变多
- 相干光通信系统的调制与解调
- ios笔试题算法_iOS 算法面试题(一)
- 单例对象会被jvm的gc时回收吗_【PHP设计模式】单例模式
- freemarker mysql 生成bean_基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(v6.6.6版)...
- 【NLTK基础】一文轻松使用NLTK进行NLP任务(附视频)
- Redis脚本实现分布式锁
- 高配置服务器组装电脑,小白DIY装机需睁大眼睛!点评几款网购组装电脑主机配置单...
- 女性有十大超能力,你知道吗?
- 低成本成FS68001A、FS68003无线充SOC芯片
- linux中查看磁盘配额的数量,[Linux实用命令]-11-磁盘配额实例详解
- 通信频段详解(5G)
- 化妆品展示网页设计作业 静态HTML化妆品网站 DW美妆网站模板下载 大学生简单网页作品代码 个人网页制作 学生个人网页设计作业
热门文章
- Zuul:构建高可用网关之多维度限流
- Guava 源码分析(Cache 原理)
- pandas版本_Datawhale十二月Pandas组学习打卡Task00.准备工作
- python xarray DataArray 用法
- c++ 调用python
- 高达82 fps的实时文本检测,可微分二值化模块
- all the input arrays must have same number of dimensions
- twisted Unhandled error in Deferred scrapy
- opencv python 图片腐蚀和膨胀
- Hadoop核心机制详细解析