React Hooks 实战指南:自定义 Hook 设计与组件优化
December 19, 2024 (1y ago)
简要概述:本文讲解了如何使用hook来做好react的组件设计,拥有了这些知识,你将在hooks的使用的和优化有更深的理解,从而设计出 更为优秀的代码
简单的回顾
这里我们先来回顾一下hook的api
- 基础的hook
- useState
- useEffcet
- useContenxt
- 扩展的hook
- useRef
- useMemo
- useCallback
- useReduce
- useImperativeHandle ( 可以让你在使用 ref 时,自定义暴露给父组件的实例值。 需要与useRef结合在一起使用 )
// 可以让你在使用 ref 时,自定义暴露给父组件的实例值。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
// 在父组件上,父组件就能调用,子组件的focus方法
inputRef.current.focus()。
- [x] useDebugValue ( 可以为自定义的hook打下标记 方便bug的调试 )
// 可用于在 React 开发者工具中显示自定义 hook 的标签,我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。当然了再deBug时也有奇效
// 注意,这个东西只有在自定义hook组件上有用
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}实战1-入门实战自定义hook如何抽离逻辑
了解了上述的一些简单的实例 和文档思路之后,我们来看一些如何进行工程实践,工程实践是检验真理的唯一标准
需求定义
我们的需求比较的简单,就是单纯的实现一个TodoList **请求接口数据,展示数据****,**非常的简单
设计思路
- 一般的设计思路
- 设计两个hook 用来保存数据 一个list列表,一个是page分页讯息 ,还有一些是辅助来实现loading的
- 使用useEeffct来处理副作用
// PostsTodo 这个组件主要实现了下面这样的功能:1.展示posts 2.展示todo
import React, { useState, useRef, useEffect, useCallback, useContext } from 'react'
/**
* Preset
*/
const PostsTodo: React.FC<{}> = () => {
/**
* state
*/
// PostsAndTodos.js
const [posts, setPosts] = useState([]);
const [isPostsLoading, setIsPostsLoading] = useState();
const [todos, setTodos] = useState([]);
const [isTodosLoading, setIsTodosLoading] = useState();
/**
* method
*/
/**
* effct
*/
useEffect(() => {
const loadPosts = async () => {
setIsPostsLoading(true);
try {
let response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=5"
);
let data = await response.json();
setPosts(data);
} catch (e) {
console.log(e);
}
setIsPostsLoading(false);
};
loadPosts();
}, []);
useEffect(() => {
// 设置方法,设计一个获取数据的initvalue函数
const loadPosst = async () => {
setIsPostsLoading(true);
try {
let respose = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
let data = await respose.json();
setPosts(data);
} catch (error) {
console.warn(error);
}
setIsPostsLoading(false);
}
// 获取todo
const loadtodo = async () => {
setIsPostsLoading(true);
try {
let respose = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
let data = await respose.json();
setPosts(data);
} catch (error) {
console.warn(error);
}
setIsPostsLoading(false);
}
// 进行调用
loadPosst()
loadtodo()
}, [])
/**
* componentsConfig
*/
/**
* render
*/
return (
<div>
<h1>posts</h1>
<ul>
{isPostsLoading ? (<div>loading....</div>) : (
posts.map((item, i) => <li key={i}> {item?.title} </li>)
)}
</ul>
<hr />
<h1>todos</h1>
<ul>
{isTodosLoading ? (<div>loading....</div>) : (
todos.map((item, i) => <li key={i}> {item?.title} </li>)
)}
</ul>
</div>
)
}
export default PostsTodo- 如何使用自定义hook来进行封装和抽取呢?,我们来看下面的例子
// useRequest
import React, { useState, useRef, useEffect, useCallback, useContext } from 'react'
/**
* Preset
*/
const useRequest = (url) => {
/**
* state
*/
// 1.保存数据
const [data, setData] = useState([]);
const [isLoading, setLoading] = useState();
const [error, setError] = useState([]);
/**
* method
*/
// 2. 发送数据的方法 注意一下,这里实际上我们可以把这个请求函数缓存起来
const loadData = useCallback(() => {
return async () => {
setLoading(true);
try {
let response = await fetch(url);
let data = await response.json();
setData(data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}
}, []
)
/**
* effct
*/
// 3. 处理副作用
useEffect(() => {
loadData()
}, [someDispatch])
/**
* componentsConfig
*/
// 4. 使用ref也可以做一些简单的缓存,但是请合理合适的运用这种技术不要滥用
/**
* render
*/
// 4. 返回需要的数据
return [data, isLoading, error]
}
export default useRequest于是我们只需要在组件中直接使用这个自定义的hook就好了
---
const [data:todo,isLoading:todoLoading,error:todoError] = useRequest('http://www.xxx.xxxx/todo')
---实战2-再次尝试自定义hook来解耦逻辑
需求定义
这一次的需求更加的简单了,稍微比之前的难一点点, 我们有一个Table ,我们希望 可以把** 请求接口数据,展示数据,****分页,查询功能**封装到一个自定义hook中,这样ui和业务逻辑就分离了,我们就可以更好的进行测试和bug的跟踪了
设计思路
为了实现这样的功能,我们的设计,也是自定义一个hooks 把上述的功能全部封装进去,当然了对于封装的粒度需要看实际的需求,有时会我们应该封装得更加细一些,有时会应该封装得粗糙一些,看需求
代码实例
useRequest.jsx
// useReuqets 注意这里有一个坑,就是如何去优化下面的这个自定义的代码,有什么方式!请你思考
import React, { useState, useRef, useEffect, useCallback, useContext } from 'react'
/**
* Preset
*/
const useReuqets = (params: any) => {
/**
* state
*/
const { url } = params;
// 是否正在请求中
const [isLoading, setIsLoanding] = useState(false);
// 请求参数
const [queryParams, setQueryParams] = useState(null);
// 请求结果
const [data, setData] = useState(null);
/**
* method
*/
// 向接口发起请求
const fetchData = async () => {
if (queryParams === null) {
return;
}
setIsLoanding(true);
const res = await jsonp({
url: url,
data: queryParams
});
setData(res);
setIsLoanding(false);
}
// 只要queryParams改变,就发起请求
useEffect(() => {
fetchData();
}, [queryParams]);
// 供外部调用
const doGet = (params) => {
setQueryParams(params);
}
/**
* componentsConfig
*/
/**
* return
*/
return [
isLoading, data, doGet
]
}
export default useReuqetsindex.jsx
import React, { useState, useEffect } from 'react';
import { Tabs, Input, RangeTime, Button, Table } from './components';
import MyFecth from './MyFetch';
const App = () => {
// ①使用数据请求hooks
const { isLoading, data, doGet } = MyFecth('http://xxx');
// 数据类型
const tabs = [{ key: 1, value: '类型1' }, { key: 0, value: '类型2' }];
const [tab, setTab] = useState(1);
// 数据ID
const [dataId, setDataid] = useState('');
// 标题
const [title, setTitle] = useState('');
// 时间区间, 默认为至今一周时间
const now = Date.now();
const [timeRange, setTimeRange] = useState([now - 1000 * 60 * 60 * 24 * 7, now]);
// 数据列表
const [dataList, setDataList] = useState([]);
// 点击搜索按钮
function handleBtnClick() {
// ②点击按钮后请求数据
const params = {};
title && (params.title = title);
dataId && (params.dataId = dataId);
params.startTime = String(timeRange[0]);
params.endTime = String(timeRange[1]);
doGet(params);
}
// ③data改变后,重新渲染列表。
// 这里相当于 componentDidUpdate。当data发生改变时,重新渲染页面
useEffect(() => {
setDataList(data);
}, [data]);
// ④首次进入页面时,无任何筛选项。拉取数据,渲染页面。
// useEffect第二个参数为一个空数组,相当于在 componentDidMount 时执行该「副作用」
useEffect(() => {
doGet({});
}, []);
return <section className="app">
<Title title="数据查询" />
<Tabs label="类型" tabs={tabs} tab={tab} onChange={setTab} />
<Input value={dataId} placeholder="请输入数据ID" onChange={setDataid}>ID</Input>
<Input value={title} placeholder="请输入数据标题" onChange={setTitle}>标题</Input>
<TimeRange label="数据时间" value={timeRange} onChange={handleTimeChange} />
<article className="btn-container">
<Button type="primary" isLoading={isLoading} onClick={handleBtnClick}>
查询
</Button>
</article>
<Table dataList={dataList}></Table>
</section>
};提出的问题
如果您认真的阅读了,上述的代码,我给你移除了一个问题,就是如何优化useRequest中的代码,给出的提示如下
useMemo可以用来缓存值,useCallback可以用来缓存函数,useRef可以用来缓存上一次的state 或者说,它可以有一定的同步作用,但是ref 还是需要慎用的,有时会会带来一些意想不到的**"意外"**
实战3-相对比较复杂的设计
声明:接下来的内容,摘自 该文章 转react-hooks,自定义hooks设计模式及其实战,如有侵权,请联系删除!
什么是自定义hooks
自定义hooks是在react-hooks基础上的一个拓展,可以根据业务需要制定满足业务需要的hooks,更注重的是逻辑单元。通过业务场景不同,我们到底需要react-hooks做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义hooks产生的初衷。
设计规范 : ****逻辑+ 组件
hooks 专注的就是逻辑复用, 是我们的项目,不仅仅停留在组件复用的层面上。hooks让我们可以将一段通用的逻辑存封起来。将我们需要它的时候,开箱即用即可。
自定义hooks-驱动条件
hooks本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks的执行,由此可见组件本身执行和hooks的执行如出一辙。
那么prop的修改,useState,useReducer使用是无状态组件更新条件,那么就是驱动hooks执行的条件。
我们用一幅图来表示如上关系。
自定义hooks-通用模式
我们设计的自定义react-hooks应该是长的这样的。

在我们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么,返回什么。
返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。正如下图所示

自定义hooks-条件限定
如果自定义hooks没有设计好,比如返回一个改变state的函数,但是没有加条件限定限定,就有可能造成不必要的上下文的执行,更有甚的是组件的循环渲染执行。

小demo:我们写一个非常简单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) } >add</button>
</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一起使用。
复杂的实战演练
我们需要实现的下功能组件有
useScroll
- 实战一:控制滚动条-吸顶效果,渐变效果-useScroll
1 首先红色色块有吸顶效果。
2 粉色色块,是固定上边但是有少量偏移,加上逐渐变透明效果。
2 自定义useScroll设计思路
- 需要实现功能:
1 监听滚动条滚动。
2 计算吸顶临界值,渐变值,透明度。
3 改变state渲染视图。
socrollTest.jsx
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 } = scrollOptions
return <View style={{ position: 'static', height: '2000px' }} >
<View className='white' />
<View id='box' style={{ opacity, transform: `translateY(${top}px)` }} >
<Swiper
className='swiper'
>
<SwiperItem className='SwiperItem' >
<View className='imgae' />
</SwiperItem>
</Swiper>
</View>
<View className={suctionTop ? 'box_card suctionTop' : 'box_card'}>
<View
style={{
background: 'red',
boxShadow: '0px 15px 10px -16px #F02F0F'
}}
className='reultCard'
>
</View>
</View>
</View>
}useScroll
// 我们通过一个scrollOptions 来保存透明度 ,top值 ,吸顶开关等变量,然后通过返回一个ref作为dom元素的采集器。接下来就是hooks如果实现的。
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.offsetHeight
const radio = box.offsetHeight / 500 * 20
const handerScroll = () => {
const scrollY = window.scrollY
/* 控制透明度 */
const computerOpacty = 1 - scrollY / 160
/* 控制吸顶效果 */
const offsetTop = offsetHeight - scrollY - offsetHeight / 500 * 84
const 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 控制每一个表单的值。
2 具有表单提交,获取整个表单数据功能。
3 点击重置,重置表单功能。
useFormTest.jsx
import useFormChange from '../../hooks/useFormChange'
import './index.less'
const selector = ['嘿嘿', '哈哈', '嘻嘻']
function index() {
const [formData, setFormItem, reset] = useFormChange()
const {
name,
options,
select
} = formData
return <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>
<AtRadio
options={[
{ 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>
<AtListItem
title='当前选择'
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.current
for (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} >点击改变state</Button>
</View>
}我们可以用 useRef 和 useState达到同步效果
性能优化
用useMemo来优化setFormItem ,resetForm方法,避免重复声明,带来的性能开销。
useTableRequset-控制表格/列表
背景:当我们需要控制带分页,带查询条件的表格/列表的情况下。
- 需求
1 统一管理表格的数据,包括列表,页码,总页码数等信息
2 实现切换页码,更新数据。
- 设计思路
2 自定义useTableRequset设计思路
1 我们需要state来保存列表数据,总页码数,当前页面等信息。
2 需要暴露一个方法用于,改变分页数据,从新请求数据。
useTableRequestTest.jsx
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 } = tableData
return <View className='index' >
<View className='table' >
<View className='table_head' >
<View className='col' >技术名称</View>
<View className='col' >icon</View>
<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)}
icon
pageSize={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) return
const 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]
}- 具体设计思路分析:
1 用一个useRef来缓存是否是第一次请求数据。
2 用useState 保存返回的数据和分页信息。
3 用两个useEffect分别处理,对于列表查询条件的更改,或者是分页状态更改,启动副作用钩子,重新请求数据,这里为了区别两种状态更改效果,实际也可以用一个effect来处理。
4 暴露两个方法,分别是请求数据和处理分页逻辑。
- 性能优化
1 我们用一个useRef来缓存是否是第一次渲染,目的是为了,初始化的时候,两个useEffect钩子都会执行,为了避免重复请求数据。
2 对于请求数据和处理分页逻辑,避免重复声明,我们用useMemo加以优化。
需要注意的是,这里把请求数据后处理逻辑连同自定义hooks封装在一起,在实际项目中,要看和后端约定的数据返回格式来制定属于自己的hooks。
useDrapDrop-控制拖拽效果
背景:用transform和hooks实现了拖拽效果,无需设置定位。
- useDrapDrop具体实现思路
实现页面的模块的脱拖拽效果
- 需要实现的功能:
1 通过自定义hooks计算出来的 x ,y 值,通过将transform的translate属性设置当前计算出来的x,y实现拖拽效果。
2 自定义hooks能抓取当前dom元素容器。
useDraDropTest.jsx
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)`}}
>drop1</View>
<View
className='drop2'
ref={dropRef2}
style={{transform:`translate(${style2.x}px, ${style2.y}px)`}}
>drop2</View>
<View
className='drop3'
>drop3</View>
</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.X
let 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来专门产生组件更新。
2 由于我们用的是transfrom改变位置,所以需要保存一下当前位置和上一次transformuchmove ,ontouchend等事件。
总结和回顾
实际上就是善于利用 useState useMemo useCallback useRef这些api 深入理解构造出高性能的前端组件设计,还是一句话,实践出真知 实践出真知!
