作者水平有限,本篇正在被重构。另外,不知道什么时候下线了本篇,可能是之前更新什么文章误操作。
新文章地址(漫长的输出过程...)
react-hooks的玩法 参照一句名言。有状态的组件无渲染,有渲染的组件无状态。这句话的意思是说,我们写组件时应该把ui和逻辑分开(逻辑用hooks来写),而hooks本身就是一堆特殊的函数,基于函数的封装也适用于hooks,这才是hooks的真谛。
学习ecomfe/react-hooks 俗话说的好,能站在巨人的肩膀上就站在巨人的肩膀上。。。好的学习方式就是看人家怎么写。
useMethods 作者提倡把常用的数据结构都封装成hooks,这样既有语义化又可以方便的拼接其他hooks。 按照作者知乎上的解释,这个hooks最初的思路是这样的。
function useMethods (initValue,methods ) { const [state,setState] = useState(initValue) const bounds = Object .entries(methods).reduce((acc,[key,fn] )=> { acc[key] = (...arg )=> { setState(value => { fn(value,...arg) }) } return acc },{}) return [state,bounds] } const arrayMethods = { push(state, item) { return state.concat(item); }, pop(state) { return state.slice(0 , -1 ); }, slice(state, start, end) { return state.slice(start, end); }, empty() { return []; }, set(state, newValue) { return newValue; }, remove(state, item) { const index = state.indexOf(item); if (index < 0 ) { return state; } return [...state.slice(0 , index), ...state.slice(index + 1 )]; } } const useArray = (initValue )=> { return useMethods(initValue,arrayMethods) } const [list,{push,pop}] = useArray([])
在最新版的huse源码中,作者基于这个思路把useMethods拆成了四个方法。首先引入了use-immer 这个包,然后也提供了无use-immer的版本,然后把bounds的代码拆成/useMethodsExtension(Native)?/。
whats-the-difference-between-usestate-and-useimmer
Boolean 基于useMethodsNative,很快扩展出useBoolean,useBoolean与useBoolean三个 hook 。
Collection 同理,array、set、map也是基于useMethodsNative扩展的。源码
Document Event import {useRef, useLayoutEffect} from 'react' ;type EventNames = keyof DocumentEventMap;type DocumentEventHandler<K extends EventNames> = (e: DocumentEventMap[K] ) => any ;export function useDocumentEvent <K extends EventNames >( eventName: K, fn: DocumentEventHandler<K>, options?: boolean | AddEventListenerOptions ) { const handler = useRef(fn); useLayoutEffect( () => { handler.current = fn; }, [fn] ); useLayoutEffect( () => { const trigger: DocumentEventHandler<K> = e => handler.current(e); document .addEventListener(eventName, trigger, options); return () => document .removeEventListener(eventName, trigger, options); }, [eventName, options] ); }
真是一切能用hooks重写的,最终要用hooks重写。用useRef存储回调函数,然后用useLayoutEffect更新handler.current。最后再用一个useLayoutEffect来监听事件。
Click Outside import {RefObject} from 'react' ;import {useDocumentEvent} from '@huse/document-event' ;export function useClickOutside(ref: RefObject<HTMLElement>, callback: (e: MouseEvent | TouchEvent) => void) { const testAndTrigger = (e: MouseEvent | TouchEvent ) => { if (!ref.current?.contains(e.target as Element)) { callback(e); } }; useDocumentEvent('mouseup' , testAndTrigger, true ); useDocumentEvent('touchstart' , testAndTrigger, true ); }
核心是利用Node.contains方法判断被点击的元素是否在目标元素之外。
Previous Value usePreviousValue export function usePreviousValue <T >(value: T ): T | undefined { const cache = useRef<T | undefined >(undefined ); useEffect( () => { cache.current = value; }, [value] ); return cache.current; }
核心是利用useRef的不变性与useEffect的执行时机晚(相当于mounted)这一特性,return cache.current执行时,虽然value变了,但是useEffect并没有执行。简简单单的几行代码实现了缓存上一次值得功能。
usePreviousEquals export function usePreviousEquals <T >(value: T, equals: CustomEquals<T> = shallowEquals ): boolean { const previousValue = usePreviousValue(value); return equals(previousValue, value); }
比对previousValue与value是否相等,第二个参数可以传入一个比对函数,默认是浅比较。
useOriginalCopy 挺费解的一个hooks(其实挺有用),主要是官方文档写的太精简了。
源代码
export function useOriginalCopy <T >(value: T, equals: CustomEquals<T> = shallowEquals ): T { const cache = useRef<T>(value); const equalsRef = useRef(equals); useEffect( () => { equalsRef.current = equals; }, [equals] ); useEffect( () => { if (!equalsRef.current(cache.current, value)) { cache.current = value; } }, [value] ); return equals(cache.current, value) ? cache.current : value; }
利用useRef缓存了value与equals的值,每次value有变化时,比对value与上次缓存是否一致,不一致则替换缓存。最后根据比对情况返回适当的值。
官方例子太难懂了。换个好理解的。
import {useEffect, useRef, useState} from 'react' import {useOriginalCopy} from '@huse/previous-value' import shallowEquals from 'shallowequal' ;export default function App ( ) { const [value,setValue] = useState({ name:'hhh' }) const value2 = useOriginalCopy(value,shallowEquals) useEffect(() => { console .log('useEffect1' ) },[value]) useEffect(() => { console .log('useEffect2' ) },[value2]) return ( <div className="App" > <h1>{`Hello CodeSandbox${value.name} ` }</h1> <h2>Start editing to see some magic happen!</ h2> <button onClick={e=>{ setValue({ name:'hhh' }) }}>change</button> </ div> ); }
每次点击change,看控制台
可以发现,除了最初的useEffect2以外,全是useEffect1。这就是useOriginalCopy的作用。
useOriginalDeepCopy 与useOriginalDeepCopy类似,不同的是第二个参数是一个深拷贝。
Merged Ref useMergedRef 从源码来看,核心代码是这个
function mergeRefs <T >(refs: Array <RefLike<T>> ): RefCallback <T > { return (value: T ) => { for (const ref of refs) { if (isCallbackRef(ref)) { ref(value); } else if (ref) { (ref as MutableRefObject<T>).current = value; } } }; }
不论是函数型的ref还是非函数型的ref,都包装成一个唯一的函数型ref,函数体是个循环,依次判断每个ref的类型,如果是函数型就执行这个函数,反之ref.current=value。
export function useScrollIntoView ( ref: RefObject<HTMLElement>, active: boolean = true , options: boolean | ScrollIntoViewOptions = {behavior: 'smooth'} ): void { const scrollOptions = useRef(options); useEffect( () => { scrollOptions.current = options; }, [options] ); useEffect( () => { if (ref.current && active) { ref.current.scrollIntoView(scrollOptions.current); } }, [ref, active] ); }
从源码来看,应该是在dom mounted或者dom更新时,并且active为true时调用scrollIntoView
const EVENT_OPTIONS = hasPassiveEvent ? {passive: true } as AddEventListenerOptions : false ;export function useScrollPosition (target?: Target ): ScrollPosition { const rafTick = useRef(0 ); const [position, setPosition] = useState(INITIAL_POSITION); useEffect( () => { if (target === null ) { return ; } const targetNode = getTargetNode(target); const targetElement = targetNode === document ? document .documentElement : targetNode as HTMLElement; setPosition(getScrollPosition(targetElement)); const syncScroll = () => { if (rafTick.current) { return ; } const callback = () => { setPosition(getScrollPosition(targetElement)); rafTick.current = 0 ; }; rafTick.current = requestAnimationFrame(callback); }; targetNode.addEventListener('scroll' , syncScroll, EVENT_OPTIONS); return () => { targetNode.removeEventListener('scroll' , syncScroll, EVENT_OPTIONS); cancelAnimationFrame(rafTick.current); }; }, [target] ); return position; }
核心思路是通过监听元素/document的滚动事件,来获取元素的scrollLeft,scrollTop。值得一提的是先是检测浏览器是否支持passive属性,如果支持,设置passive为true,以提高滚动性能。然后是获取元素的scrollTop、scrollLeft这些属性会引发浏览器回流,而scroll事件又是高频触发的,所以为了减少浏览器回流,必须确保一帧内只回流一次,那个rafTick就是干这个的。
export function useScrollLeft (element?: HTMLElement | null ): number { const {scrollLeft} = useScrollPosition(element); return scrollLeft; }
export function useScrollTop (element?: HTMLElement | null ): number { const {scrollTop} = useScrollPosition(element); return scrollTop; }
Timeout useTimeout useInterval useStableInterval export function useStableInterval(callback: (() => any) | undefined, time: number): void { const fn = useRef(callback); useEffect( () => { fn.current = callback; }, [callback] ); useEffect( () => { if (time < 0 ) { return ; } let tick: any = null ; let running = true ; const trigger = () => { const next = () => { if (running) { tick = setTimeout(trigger, time); } }; if (!fn.current) { next(); return ; } const returnValue = fn.current(); if (typeof returnValue?.then === 'function' ) { returnValue.then(next, next); } else { next(); } }; tick = setTimeout(trigger, time); return () => { running = false ; clearTimeout(tick); }; }, [time] ); }
稳定的Interval?看代码是利用setTimeout模拟了setInterval
Debug useRenderTimes export function useRenderTimes ( ): number { const count = useRef(0 ); count.current++; return count.current; }
言简意赅。和我预想的实现方式一样。
useChangeTimes export function useChangeTimes <T >(value: T ): number { const count = useRef(0 ); const previousValue = usePreviousValue(value); const mounted = useRef(false ); useEffect( () => { mounted.current = true ; }, [] ); if (mounted.current && value !== previousValue) { count.current++; } return count.current; }
记录组件mounted之后,某个值/状态变化的次数。
useUpdateCause 找出state/props更新的原因?加强版的console.trace?好像不是。
function findUpdateCause <T extends Record <string , any >>(previous: T, current: T ): UpdateCause [] { const causes = [] as UpdateCause[]; const keys = Object .keys(previous); for (const key of keys) { const previousValue = previous[key]; const currentValue = current[key]; if (previousValue !== currentValue) { const cause: UpdateCause = { previousValue, currentValue, propName: key, shallowEquals: shallowEquals(previousValue, currentValue), deepEquals: deepEquals(previousValue, currentValue), }; causes.push(cause); } } return causes; }
核心代码。记录某个值previous与value,把结果存到数组里。
Derived State useDerivedState export function useDerivedState <P , S = P >( propValue: P, compute: Derive<P, S> = v => v as unknown as S ): [S , Dispatch <SetStateAction <S >>] { const [previousPropValue, setPreviousPropValue] = useState(propValue); const [value, setValue] = useState(() => compute(propValue, undefined )); if (previousPropValue !== propValue) { setValue(state => compute(propValue, state)); setPreviousPropValue(propValue); } return [value, setValue]; }
以上两处核心代码做的事其实就是造了一个state(上一次compute函数的返回值),在props有变化时作为compute函数的参数。
感觉理解起来好别扭,为啥不直接用useMemo,而且很灵活,有很多业务场景用到了这种别捏的实现吗?
Effect Ref useEffectRef export function useEffectRef <E extends HTMLElement = HTMLElement >(callback: RefCallback<E> ): EffectRef <E > { const disposeRef = useRef<(( ) => void )>(noop ); const effect = useCallback ( (element: E | null ) => { disposeRef.current( ); disposeRef.current = noop; if (element ) { const dispose = callback(element ); if (typeof dispose === 'function ' ) { disposeRef.current = dispose; } else if (dispose !== undefined ) { console .warn('Effect ref callback must return undefined or a dispose function ' ); } } }, [callback] ); return effect ; }
初次看这段代码,不是很理解为什么要造这么个轮子,本质上是要解决什么问题,文档 上的那个例子直接加个if
const updateMessage = useCallback( (element) =>{ if (element) setMessage( `Root element is changed to <${element.nodeName.toLowerCase()} >` ) }, [] ); {createElement(tag, { ref :updateMessage }, <p style={{ color : 'red' }}>{message}</p>)} / /...
不是也可以嘛,后来看到关于disposeRef 的代码,才有点明白,updateMessage可以返回一个函数,在下次dom更新时执行这个函数,可以做很多事。
import {useState, useCallback} from 'react' ;interface ChangeEvent { target: {value: string }; } export interface InputValueState { value: string ; onChange(e: ChangeEvent): void ; } export function useInputValue(initialValue: string = ''): InputValueState { const [value, setValue] = useState(initialValue); const onChange = useCallback( (e: ChangeEvent) => setValue(e.target.value), [] ); return {value, onChange}; }
乍一看,短短几行代码没明白这个hook的作用,直到看到文档上的例子才恍然大悟,value和onChange就是放在一起用的。
//... const age = useInputValue(10); //... <Form.Item label="Age" name="age"> <Input type="number" {...age} /> </Form.Item>
react-hooks版的v-model,倒是挺小而美的。
Local Storage useLocalStorage import {useState, useEffect, useCallback} from 'react' ;function getStorage <T >(key: string , initialValue: T ): T { if (!window ?.localStorage) { return initialValue; } try { const stringValue = window .localStorage.getItem(key); return stringValue ? JSON .parse(stringValue) : initialValue; } catch { return initialValue; } } export function useLocalStorage <T >(key: string , initialValue: T ): [T , (value: T ) => void ] { const [value, setValue] = useState(() => getStorage<T>(key, initialValue)); const setStorageValue = useCallback( (value: T) => { window ?.localStorage?.setItem(key, JSON .stringify(value)); setValue(value); }, [key] ); useEffect( () => { if (!window ?.localStorage) { return ; } const notify = (e: StorageEvent ) => { if (e.storageArea === localStorage && e.key === key) { try { setValue(e.newValue ? JSON .parse(e.newValue) : initialValue); } catch { setValue(initialValue); } } }; window .addEventListener('storage' , notify); return () => { window .removeEventListener('storage' , notify); }; }, [initialValue, key] ); return [value, setStorageValue]; }
这个hooks分两部分,首先是一个普通的工具方法getStorage,作用是从localStorage中拿到目标key所对应的数据,其次是useLocalStorage这个hooks,实现思路是监听storage事件,“实时”的获取key所对应的数据,然后返回value和更新value的方法(同时会更新value与localstroage)
值得一提的是,标注1那里有个e.storageArea === localStorage的代码,这里算是学到了,因为sessionstorage的改变也会触发storage。
看着名字第一感觉还以为是对视频、图片等多媒体操作呢,结果一看源码,是关于媒体查询的。(又学到了新知识,以前一直以为媒体查询那些语法只能用于css)
const matchMedia = (query: string ) => { const watcher = window .matchMedia(query); return { watcher, matches: watcher.matches, }; }; export function useMedia (query: string ): boolean { const [matched, setMatched] = useState(() => matchMedia(query).matches); useEffect( () => { const {watcher} = matchMedia(query); const onChange = () => setMatched(!!watcher.matches); watcher.addListener(onChange); return () => watcher.removeListener(onChange); }, [query] ); return matched; }
与上一个hook思路差不多,一个功能函数,一个hooks主体。主要是window.matchMedia(query)这个方法,其中的query可以是一个媒体查询语法,具体文档参考这里 。
标注1好像运行不了,要写成addEventListener才可以,具体可以参考这个question 。
usePreferDarkMode 内部调用了useMedia,比较简单。
Update useForceUpdate import {useReducer} from 'react' ;export function useForceUpdate ( ) { return useReducer((v: number ) => v + 1 , 0 )[1 ]; }
感觉用useReducer有点大材小用(其实是我对useReducer不熟),也可以用useState模拟。
import { useState } from "react" export const useForceUpdate = () => { const [state,setState] = useState(0 ) return () => { setState(state+1 ) } }
NetWork useOnLine import {useEffect} from 'react' ;import {useSwitch} from '@huse/boolean' ;export function useOnLine ( ): boolean { const [onLine, goOnLine, goOffLine] = useSwitch(navigator.onLine ?? true ); useEffect( () => { window .addEventListener('online' , goOnLine); window .addEventListener('offline' , goOffLine); return () => { window .removeEventListener('online' , goOnLine); window .removeEventListener('offline' , goOffLine); }; }, [goOnLine, goOffLine] ); return onLine; }
核心思路是监听window的online与offline方法,然后写成hook的语法。
Transition State useTransitionState 粘贴代码没有时效性,所以源码在这里
核心代码是这一句,看实现方式有点像函数去抖```typescript const tick = setTimeout( () => setValue(defaultValue), durationRef.current ); return () => clearTimeout(tick); ``` PS: 还有很多,干不动了,以后再看