andyjxli / blog Goto Github PK
View Code? Open in Web Editor NEWmy bolg
my bolg
自 React Hooks 16.8.0 后带来了 React hooks 这一特性。这一特性在没有破坏性的更新下为我们带来了更加舒爽的开发方式。过去我们常常因providers,consumers,高阶组件,render props 等形成“嵌套地狱”。尽管 Class Component 在某种程度上为我们提供了更方便的写法以及生命周期,但同时也带来了一些不好的地方。例如难以理解的 class 内部原理、难以测试的声明周期。而 React Hooks 为我们提供了一种 Function Component 的写法,让我们用更少的代码写出更加优雅、易懂的代码。本文不做 React Hooks API的讲述,如有不懂,请移步 Hooks 简介
在开发代码时,我们发送后端请求后接受到的数据,需要使用try/catch来捕获错误。而每次捕获出的错误可能需要打印出来以检测bug。这样我们每次都会写同样的代码,这样在开发过程中很不友好。同时有些同学不习惯使用 try/catch 来捕获错误,这就可能造成不可预计的问题。
import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
type ParamsType = PageType & TimeNumberType
const reducer = (state: ParamsType, action: Actions) => {
const { payload } = action
return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
pageSize: 10,
pageNumber: 1,
startTime: 0,
endTime: 0
}
const ListComponent = () => {
const [params, dispatch] = useReducer(reducer, initialState)
const getList = async () => {
// try catch
try {
const res = await postListData(params)
console.log(res)
} catch (err) {
console.error(err)
}
}
useEffect(() => {
getList()
}, [params])
}
demo中展示了在业务场景中发送请求的场景,当发送请求多了之后我们会每次手动try / catch,虽然不是大问题,但是重复代码写多了会觉得难受...。下面看第二个功能。
在实际的业务场景中,我们向后端发送请求时,往往伴随着用户点击多次,但是只能发送一次请求的问题,这时我们需要手动加锁。并且在很多场景中我们需要知道请求状态来为页面设置loading。例如:
import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'
type ParamsType = PageType & TimeNumberType
const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
const { payload } = action
return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
pageSize: 10,
pageNumber: 1,
startTime: 0,
endTime: 0
}
const ListComponent = () => {
const [params, dispatch] = useReducer(reducer, initialState)
const [loading, setLoading] = useState(false)
const [list, setList] = useState({})
const getList = async () => {
// loading is true
if (loading) return
// set loading status
setLoading(true)
// try catch
try {
const res = await postListData(params)
setList(res)
setLoading(false)
} catch (err) {
console.error(err)
setLoading(false)
}
}
useEffect(() => {
getList()
}, [params])
return (
<div style={{ marginBottom: '20px' }}>
<DateRangePicker
onChange={handleDateChange}
/>
<Table
onPageChange={(pageNumber: number) => {
dispatch({ payload: { pageNumber }, type: PAGE })
}}
list={list}
// 数据是否正在加载,以此来判断是否需要展示loading
loading={loading}
/>
</div>
)
}
demo中展示了日期组件以及包含有分页器的 Table组件,当日期发生变更,或者分页器发生变更时,我们需要dispatch来更新请求参数,从而发送请求。在发送请求时如果正在请求,则忽略,而不在请求时需要手动加锁,来防止多次请求。
同时Table需要根据请求状态来判断是否需要展示loading。
基于以上的问题,我们能否通过 Hooks 来封装一个 custom hooks来解决问题。
custom hooks 解决的问题
所以我们需要在 custom hooks 中发送请求、暴露出请求后的值、暴露 loading 状态、以及用户可能需要多次请求,这就需要暴露一个勾子。在发生请求错误时可能需要做某些操作,所以还需要暴露在错误时回调的勾子函数。
是否立即请求并接受初始化返回值
业务我们并不希望初始化的是否立即发送请求。
并且能够有初始化的返回值
支持泛型
在TS中,开发者希望能够自定义请求的参数类型,以及请求结果的类型
useFetch 函数
import { useState, useEffect } from "react";
/**
* 1. 解决每个函数都要统一写try/catch的流程
* 2. 解决发送请求需要手动加锁防止多次重复请求的痛点
* 3. 不需要在手动useState loading了~,直接获取fetching值
* 4. (甚至在参数发生变化时只需要传入更改的参数就OK)已删除
* @param getFunction 发送请求的函数
* @param params 参数
* @param initRes 初始化值
* @param execute 是否立即执行请求函数
*/
// R, P支持泛型
function UseFetch<R, P>(
getFunction: any,
params: P,
initRes?: R,
execute: boolean = true
): [
R,
boolean,
(params?: Partial<P>) => void,
(fn?: (err: any) => void) => void
] {
type ErrorFunction = ((fn?: (err: any) => void) => void) | null;
const [res, setRes] = useState(initRes as R);
const [fetching, setFetch] = useState(false);
const [failed, setFailed] = useState<ErrorFunction>(null);
// 参数也许并不是每次都完整需要 Partial<P>
const fetchData: (params?: Partial<P>) => void = async (params?: any) => {
if (fetching) return;
setFetch(true);
try {
setRes(await getFunction(params));
setFetch(false);
} catch (err) {
console.error(err);
setFetch(false);
failed && failed(err);
}
};
const setError: ErrorFunction = fn => fn && setFailed(fn);
// 首次执行只请求一次
useEffect(() => {
execute && fetchData(params);
}, []);
/**
* res 返回的数据
* fetching 是否在请求中
* fetchData 手动再次触发请求
* setError 当发生请求错误时,需要执行的回掉函数
*/
return [res, fetching, fetchData, setError];
}
const useFetch = UseFetch;
export default useFetch;
根据最初的demo我们改造一下代码
import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'
// 导入 useFetch
import { useFetch } from 'custom-hooks'
type ParamsType = PageType & TimeNumberType
type ListInfo = {list: Array<any>, total: number}
const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
const { payload } = action
return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
pageSize: 10,
pageNumber: 1,
startTime: 0,
endTime: 0
}
const ListComponent = () => {
const [params, dispatch] = useReducer(reducer, initialState)
const [list, loading, getList] = useFetch<ListInfo, ParamsType>(
getWithDraw,
state,
{ list: [], total: 0 },
false
)
useEffect(() => {
getList()
}, [params])
return (
<div style={{ marginBottom: '20px' }}>
<DateRangePicker
onChange={handleDateChange}
/>
<Table
onPageChange={(pageNumber: number) => {
dispatch({ payload: { pageNumber }, type: PAGE })
}}
list={list}
// 数据是否正在加载,以此来判断是否需要展示loading
loading={loading}
/>
</div>
)
}
对比代码我们可以看到中间的请求的代码被我们干掉了,使用 useFetch 来将状态以及发送请求封装在一起。能够让我们写更少的代码。
也许有些请求不需要关注请求状态
// 解构赋值、空着就好
const [list, , getList] = useFetch<ListInfo, ParamsType>(
getWithDraw,
state,
{ list: [], total: 0 },
false
)
本文完~
如有问题,欢迎指出~
本文使用 React Hooks 构建一个监听 DOM 元素曝光的 Custom Hooks,主要功能是监听 DOM 元素是否在规定内曝光,从而可以完成曝光打点。
过去我们监听 DOM 元素都是通过监听 scroll 事件来监听目标元素是否在可视区,这样我们需要获取目标元素的一些数据。这样似乎很麻烦,而通过 Intersection Observer 来完成监听,更加方便、友好。
MDN: IntersectionObserver 接口 (从属于 Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。
其他相关概念可见 谈谈 IntersectionObserver 懒加载 、 IntersectionObserver API 使用教程
简单来说这个对象会观察目标元素,当目标元素与它的祖先元素或者可视区发生交叉时,会触发回调函数。
通过这个对象我们可以监听需要曝光打点的元素,更方便的实现打点。同时我们不需要监听 scroll 事件以及相对应的 DOM 操作,减少了性能的开销。
为了能够在不同的场景中使用,我们可以将其封装成 HOOKS,更方便的调用该方法
// useIntersectionObserver.ts
// 定义参数函数类型以及返回值类型
import { useState, useCallback, useEffect } from 'react'
type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any>[]>>]
function UseIntersectionObserver(
observerList: ObserverList,
callback: CallbackFunction,
infinite: boolean = false,
opt: IntersectionObserverInit = {}
): ResultType {
// list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list
const [list, setList] = useState<ObserverList>(observerList)
// intersectionObserver: 观察者对象
let intersectionObserver: IntersectionObserver | null = null
// ...
return [setList]
}
const useIntersectionObserver = UseIntersectionObserver
export default useIntersectionObserver
UseIntersectionObserver 函数参数:
UseIntersectionObserver 返回值:
定义 observeExposure 函数
// UseIntersectionObserver
const observeExposure = useCallback((list: ObserverList) => {}, [])
使用 useCallback 减少不必要的重复函数声明
判断浏览器环境以及 list 的是否为空
if (typeof IntersectionObserver === 'undefined') {
throw new Error('Current browser does not support IntersectionObserver ')
}
if (list.length === 0) return
构造新的观察者实例
// 当观察者存在时销毁该对象
intersectionObserver && intersectionObserver.disconnect()
// 构造新的观察者实例
intersectionObserver = new IntersectionObserver(entries => {
// 保存本次监听被曝光的元素
let activeList: NumberList = []
// 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用callback函数,并且取消监听
entries.forEach(entrie => {
// 找出本次被监听对象在list中的索引
const index = Array.from(list).findIndex(
item => item.current === entrie.target
)
// 防止意外发生
if (index === -1) return
// isIntersecting是每个被监听的元素所自带的属性,若为ture,则表示被曝光
// 并且未被曝光过
if (entrie.isIntersecting) {
// 保存本次曝光元素索引
activeList.push(index)
// 解除观察, 若需要无限观察则不取消监听
!infinite &&
intersectionObserver &&
intersectionObserver.unobserve(list[index].current)
}
})
// callback函数
activeList.length > 0 && callback(activeList)
}, opt)
使用 intersectionObserver 监听 list 中的元素
// 递归观察每一个元素
list.forEach(item => {
item.current &&
intersectionObserver &&
intersectionObserver.observe(item.current)
// 可以兼容直接传入DOM节点。
// if((<React.RefObject<any>>item).current) {
// intersectionObserver.observe((<React.RefObject<any>>item).current)
// } else if ((<HTMLElement>item)) {
// intersectionObserver.observe((<HTMLElement>item))
// }
})
useEffect(() => {
observeExposure(list)
// 当 umount 时取消链接
return () => {
intersectionObserver && intersectionObserver.disconnect()
}
}, [list])
import { useState, useCallback, useEffect } from 'react'
type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any>[]>>]
/**
* UseIntersectionObserver
* @param observerList 由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象
* @param callback 当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList数组中的索引组成
* @param infinite 是否持续观察目标元素,默认值为false。(因为曝光打点一般只需上报一次)
* @param opt 可以自定义曝光条件(值的构成参考MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调
*/
function UseIntersectionObserver(
observerList: ObserverList,
callback: CallbackFunction,
infinite: boolean = false,
opt: IntersectionObserverInit = {}
): ResultType {
// list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list
const [list, setList] = useState<ObserverList>(observerList)
// intersectionObserver: 观察者对象
let intersectionObserver: IntersectionObserver | null = null
const observeExposure = useCallback((list: ObserverList) => {
if (!IntersectionObserver) {
throw new Error('Current browser does not support IntersectionObserver ')
}
if (list.length === 0) return
// 当观察者存在时销毁该对象
intersectionObserver && intersectionObserver.disconnect()
// 构造新的观察者实例
intersectionObserver = new IntersectionObserver(entries => {
// 保存本次监听被曝光的元素
let activeList: NumberList = []
// 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用callback函数,并且取消监听
entries.forEach(entrie => {
// 找出本次被监听对象在list中的索引
const index = Array.from(list).findIndex(
item => item.current === entrie.target
)
// 防止意外发生
if (index === -1) return
// isIntersecting是每个被监听的元素所自带的属性,若为ture,则表示被曝光
// 并且未被曝光过
if (entrie.isIntersecting) {
// 保存本次曝光元素索引
activeList.push(index)
// 解除观察, 若需要无限观察则不取消监听
!infinite &&
intersectionObserver &&
intersectionObserver.unobserve(list[index].current)
}
})
// callback函数
activeList.length > 0 && callback(activeList)
}, opt)
list.forEach(item => {
item.current &&
intersectionObserver &&
intersectionObserver.observe(item.current)
// 可以兼容直接传入DOM节点。
// if((<React.RefObject<any>>item).current) {
// intersectionObserver.observe((<React.RefObject<any>>item).current)
// } else if ((<HTMLElement>item)) {
// intersectionObserver.observe((<HTMLElement>item))
// }
})
}, [])
useEffect(() => {
observeExposure(list)
// 当 umount 时取消链接
return () => {
intersectionObserver && intersectionObserver.disconnect()
}
}, [list])
return [setList]
}
const useIntersectionObserver = UseIntersectionObserver
export default useIntersectionObserver
实现一个简单的商品列表曝光打点的案例
import Card from 'components/goods-card/goods-card'
import { connect } from 'react-redux'
import { getSinglePromotionList } from '../../page_components/promotion/redux/creator'
import React, { useEffect, useState, useCallback } from 'react'
import useIntersectionObserver from 'page_components/promotion/useIntersectionObserver'
const List = (props: { info: any; getData: any }) => {
const { info, getData } = props
// 被监听元素的列表
const [refList, setRefList] = useState<React.RefObject<any>[]>([])
const callback = useCallback((indexList: number[]) => {
console.log(indexList)
}, [])
// 调用
const [setList] = useIntersectionObserver(refList, callback)
// 当refList发生改变时,调用我们的Hook返回的方法以更新需要监听的元素
useEffect(() => {
setList(refList)
}, [refList])
// 当数据发生改变时,重新生成RefList
useEffect(() => {
const list: React.RefObject<any>[] = info.list.map(() => React.createRef())
setRefList(list)
}, [info])
// 发送请求,获取商品数据
useEffect(() => {
getData()
}, [])
return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{info.list.map((item: any, index: number) => (
<div ref={refList[index]} key={index}>
<Card card={item} />
</div>
))}
</div>
)
}
const mapStateToProps = (state: any) => {
return {
info: state.promotionStore.singlePromotionInfo,
userInfo: state.userInfo
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
getData: () => dispatch(getSinglePromotionList(params, silence))
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(List)
案例效果
从动图可以看出,当 card 被曝光时打印出索引值,而已经被曝光的值不会再次曝光。
本文完~
更多文章。 我的掘金~
如有疑问,欢迎指出~
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.