学习react-hooks之huse

作者 likaiqiang 日期 2021-05-26
学习react-hooks之huse

作者水平有限,本篇正在被重构。另外,不知道什么时候下线了本篇,可能是之前更新什么文章误操作。

新文章地址(漫长的输出过程...)

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的作用。
18f3c777-503c-4528-86f0-17b1c1e6994b-image.png

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) {
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065
(ref as MutableRefObject<T>).current = value;
}
}
};
}

不论是函数型的ref还是非函数型的ref,都包装成一个唯一的函数型ref,函数体是个循环,依次判断每个ref的类型,如果是函数型就执行这个函数,反之ref.current=value。

Scroll Into View

useScrollIntoView

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

Scroll Position

useScrollPosition

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;
} // 避免一帧内触发两次callback

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就是干这个的。

useScrollLeft

export function useScrollLeft(element?: HTMLElement | null): number {
const {scrollLeft} = useScrollPosition(element);
return scrollLeft;
}

useScrollTop

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;
}

// To get rid of type checking on Node's `setTimeout` function
let tick: any = null;
let running = true;
const trigger = () => {
const next = () => {
// It is possible that when `clearTimeout` is happening an async callback is in pending state,
// then when this callback resolves it will continue to start a new timer,
// therefore we need a `running` flag to indicate whether a future timer is able to start.
if (running) { //如果已经clearTimeout,running为false,这时候假如异步函数resolve了,不会执行条件语句
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时,设置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();
// To ensure every dispose function is called only once.
disposeRef.current = noop;
if (element) {
const dispose = callback(element);

if (typeof dispose === 'function') {
disposeRef.current = dispose;
}
// Have an extra type check to work with javascript.
else if (dispose !== undefined) {
// eslint-disable-next-line no-console
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更新时执行这个函数,可以做很多事。

Input Value

useInputValue

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) => {
// eslint-disable-next-line no-unused-expressions
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) { // 标注1
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。

Media

useMedia

看着名字第一感觉还以为是对视频、图片等多媒体操作呢,结果一看源码,是关于媒体查询的。(又学到了新知识,以前一直以为媒体查询那些语法只能用于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); //标注1
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: 还有很多,干不动了,以后再看