Navigation
阅读进度0%
No headings found.

React Hooks 实战指南:自定义 Hook 设计与组件优化

December 19, 2024 (1y ago)

React
Hooks
JavaScript

简要概述:本文讲解了如何使用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 **请求接口数据展示数据****,**非常的简单

设计思路

  1. 一般的设计思路
  • 设计两个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
  1. 如何使用自定义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 useReuqets

index.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 深入理解构造出高性能的前端组件设计,还是一句话,实践出真知 实践出真知!