WHAT - 通过 react-use 源码学习 React(UI 篇)
目录
- 一、官方介绍
- 1. Sensors
- 2. UI
- 3. Animations
- 4. Side-Effects
- 5. Lifecycles
- 6. State
- 7. Miscellaneous
- 二、源码学习
- 示例:n. xx - yy
- UI - useAudio
- `createHTMLMediaHook` 函数解析
- 功能
- 参数
- **返回值**
- **实现细节**
- **总结**
一、官方介绍
Github 地址
react-use 是一个流行的 React 自定义 Hook 库,提供了一组常用的 Hook,以帮助开发者在 React 应用程序中更方便地处理常见的任务和功能。
官方将 react-use
的 Hook 分成了以下几个主要类别,以便更好地组织和查找常用的功能。每个类别涵盖了不同类型的 Hook,满足各种开发需求。以下是这些类别的详细说明:
1. Sensors
- 功能: 主要涉及与浏览器或用户交互相关的传感器功能。
- 示例:
useMouse
: 获取鼠标位置。useWindowSize
: 获取窗口尺寸。useBattery
: 监控电池状态。
2. UI
- 功能: 涉及用户界面相关的功能,如处理样式、显示和隐藏元素等。
- 示例:
useClickAway
: 监听点击事件以检测用户点击是否发生在组件外部。useMeasure
: 测量元素的大小和位置。useDarkMode
: 管理和检测暗模式状态。
3. Animations
- 功能: 处理动画和过渡效果。
- 示例:
useSpring
: 使用react-spring
处理动画效果。useTransition
: 使用react-spring
处理过渡动画。
4. Side-Effects
- 功能: 处理副作用相关的 Hook,包括数据获取、异步操作等。
- 示例:
useAsync
: 处理异步操作,如数据获取,并提供状态和结果。useFetch
: 简化数据获取操作。useAxios
: 使用 Axios 进行数据请求的 Hook。
5. Lifecycles
- 功能: 处理组件生命周期相关的 Hook。
- 示例:
useMount
: 在组件挂载时执行的 Hook。useUnmount
: 在组件卸载时执行的 Hook。useUpdate
: 在组件更新时执行的 Hook。
6. State
- 功能: 管理组件状态和相关逻辑。
- 示例:
useState
: 提供基本状态管理功能。useReducer
: 替代useState
实现更复杂的状态逻辑。useForm
: 管理表单状态和验证。useInput
: 管理输入字段的状态。
7. Miscellaneous
- 功能: 各种其他实用功能的 Hook,涵盖一些不容易归类到其他类别的功能。
这种分类方法使得 react-use
的 Hook 更加有组织和易于查找,帮助开发者快速找到需要的功能并有效地集成到他们的应用程序中。
二、源码学习
示例:n. xx - yy
something
使用
源码
解释
UI - useAudio
plays audio and exposes its controls.
使用
import {useAudio} from 'react-use';
const Demo = () => {
const [audio, state, controls, ref] = useAudio({
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
autoPlay: true,
});
return (
<div>
{audio}
<pre>{JSON.stringify(state, null, 2)}</pre>
<button onClick={controls.pause}>Pause</button>
<button onClick={controls.play}>Play</button>
<br/>
<button onClick={controls.mute}>Mute</button>
<button onClick={controls.unmute}>Un-mute</button>
<br/>
<button onClick={() => controls.volume(.1)}>Volume: 10%</button>
<button onClick={() => controls.volume(.5)}>Volume: 50%</button>
<button onClick={() => controls.volume(1)}>Volume: 100%</button>
<br/>
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button>
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button>
</div>
);
};
源码
import createHTMLMediaHook from './factory/createHTMLMediaHook';
const useAudio = createHTMLMediaHook<HTMLAudioElement>('audio');
export default useAudio;
//./factory/createHTMLMediaHook
import * as React from 'react';
import { useEffect, useRef } from 'react';
import useSetState from '../useSetState';
import parseTimeRanges from '../misc/parseTimeRanges';
export interface HTMLMediaProps
extends React.AudioHTMLAttributes<any>,
React.VideoHTMLAttributes<any> {
src: string;
}
export interface HTMLMediaState {
buffered: any[];
duration: number;
paused: boolean;
muted: boolean;
time: number;
volume: number;
playing: boolean;
}
export interface HTMLMediaControls {
play: () => Promise<void> | void;
pause: () => void;
mute: () => void;
unmute: () => void;
volume: (volume: number) => void;
seek: (time: number) => void;
}
type MediaPropsWithRef<T> = HTMLMediaProps & { ref?: React.MutableRefObject<T | null> };
export default function createHTMLMediaHook<T extends HTMLAudioElement | HTMLVideoElement>(
tag: 'audio' | 'video'
) {
return (elOrProps: HTMLMediaProps | React.ReactElement<HTMLMediaProps>) => {
let element: React.ReactElement<MediaPropsWithRef<T>> | undefined;
let props: MediaPropsWithRef<T>;
if (React.isValidElement(elOrProps)) {
element = elOrProps;
props = element.props;
} else {
props = elOrProps;
}
const [state, setState] = useSetState<HTMLMediaState>({
buffered: [],
time: 0,
duration: 0,
paused: true,
muted: false,
volume: 1,
playing: false,
});
const ref = useRef<T | null>(null);
const wrapEvent = (userEvent, proxyEvent?) => {
return (event) => {
try {
proxyEvent && proxyEvent(event);
} finally {
userEvent && userEvent(event);
}
};
};
const onPlay = () => setState({ paused: false });
const onPlaying = () => setState({ playing: true });
const onWaiting = () => setState({ playing: false });
const onPause = () => setState({ paused: true, playing: false });
const onVolumeChange = () => {
const el = ref.current;
if (!el) {
return;
}
setState({
muted: el.muted,
volume: el.volume,
});
};
const onDurationChange = () => {
const el = ref.current;
if (!el) {
return;
}
const { duration, buffered } = el;
setState({
duration,
buffered: parseTimeRanges(buffered),
});
};
const onTimeUpdate = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ time: el.currentTime });
};
const onProgress = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ buffered: parseTimeRanges(el.buffered) });
};
if (element) {
element = React.cloneElement(element, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPlaying: wrapEvent(props.onPlaying, onPlaying),
onWaiting: wrapEvent(props.onWaiting, onWaiting),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
});
} else {
element = React.createElement(tag, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPlaying: wrapEvent(props.onPlaying, onPlaying),
onWaiting: wrapEvent(props.onWaiting, onWaiting),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
} as any); // TODO: fix this typing.
}
// Some browsers return `Promise` on `.play()` and may throw errors
// if one tries to execute another `.play()` or `.pause()` while that
// promise is resolving. So we prevent that with this lock.
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=593273
let lockPlay: boolean = false;
const controls = {
play: () => {
const el = ref.current;
if (!el) {
return undefined;
}
if (!lockPlay) {
const promise = el.play();
const isPromise = typeof promise === 'object';
if (isPromise) {
lockPlay = true;
const resetLock = () => {
lockPlay = false;
};
promise.then(resetLock, resetLock);
}
return promise;
}
return undefined;
},
pause: () => {
const el = ref.current;
if (el && !lockPlay) {
return el.pause();
}
},
seek: (time: number) => {
const el = ref.current;
if (!el || state.duration === undefined) {
return;
}
time = Math.min(state.duration, Math.max(0, time));
el.currentTime = time;
},
volume: (volume: number) => {
const el = ref.current;
if (!el) {
return;
}
volume = Math.min(1, Math.max(0, volume));
el.volume = volume;
setState({ volume });
},
mute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = true;
},
unmute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = false;
},
};
useEffect(() => {
const el = ref.current!;
if (!el) {
if (process.env.NODE_ENV !== 'production') {
if (tag === 'audio') {
console.error(
'useAudio() ref to <audio> element is empty at mount. ' +
'It seem you have not rendered the audio element, which it ' +
'returns as the first argument const [audio] = useAudio(...).'
);
} else if (tag === 'video') {
console.error(
'useVideo() ref to <video> element is empty at mount. ' +
'It seem you have not rendered the video element, which it ' +
'returns as the first argument const [video] = useVideo(...).'
);
}
}
return;
}
setState({
volume: el.volume,
muted: el.muted,
paused: el.paused,
});
// Start media, if autoPlay requested.
if (props.autoPlay && el.paused) {
controls.play();
}
}, [props.src]);
return [element, state, controls, ref] as const;
};
}
解释
createHTMLMediaHook
是一个高阶函数,用于创建 React 自定义 Hook,简化了对 <audio>
和 <video>
元素的控制。它结合了 React 的状态管理和生命周期钩子来提供一个便捷的接口,用于处理 HTML 媒体元素的播放、暂停、音量控制等操作。以下是对 createHTMLMediaHook
函数的详细解析。
createHTMLMediaHook
函数解析
功能
createHTMLMediaHook
主要用于创建处理 HTML 媒体元素(如 <audio>
和 <video>
)的 Hook。这个 Hook 封装了对媒体元素的常见操作和状态管理,提供了一个统一的接口来操作和控制媒体元素。
参数
tag
:- 类型:
'audio' | 'video'
- 说明: 指定要创建的 HTML 媒体元素类型,可以是
'audio'
或'video'
。
- 类型:
返回值
- 返回一个函数,该函数接受两种可能的参数:
elOrProps
:- 类型:
HTMLMediaProps | React.ReactElement<HTMLMediaProps>
- 说明: 可以是媒体元素的属性对象,也可以是包含媒体属性的 React 元素。
- 类型:
- 返回值是一个元组
[element, state, controls, ref]
:element
: 渲染的 React 元素(<audio>
或<video>
)。state
: 当前的媒体状态(HTMLMediaState
)。controls
: 控制媒体播放的函数(HTMLMediaControls
)。ref
: 对应媒体元素的ref
。
实现细节
-
参数处理
let element: React.ReactElement<MediaPropsWithRef<T>> | undefined; let props: MediaPropsWithRef<T>; if (React.isValidElement(elOrProps)) { element = elOrProps; props = element.props; } else { props = elOrProps; }
- 如果
elOrProps
是一个 React 元素,则提取其属性。 - 否则,
elOrProps
被认为是直接的媒体属性。
- 如果
-
状态和引用
const [state, setState] = useSetState<HTMLMediaState>({ buffered: [], time: 0, duration: 0, paused: true, muted: false, volume: 1, playing: false, }); const ref = useRef<T | null>(null);
- 使用
useSetState
管理媒体状态。有关useSetState
具体解释可以阅读 WHAT - 通过 react-use 源码学习 React(State 篇) ref
是一个useRef
,用于引用实际的媒体元素。
- 使用
-
事件处理
const wrapEvent = (userEvent, proxyEvent?) => { return (event) => { try { proxyEvent && proxyEvent(event); } finally { userEvent && userEvent(event); } }; };
-
wrapEvent
用于将用户提供的事件处理函数和内部事件处理函数组合在一起。 -
内部事件处理函数(如
onPlay
、onPause
等)更新状态以反映媒体元素的当前状态。
-
-
创建和渲染媒体元素
if (element) { element = React.cloneElement(element, { controls: false, ...props, ref, onPlay: wrapEvent(props.onPlay, onPlay), onPlaying: wrapEvent(props.onPlaying, onPlaying), onWaiting: wrapEvent(props.onWaiting, onWaiting), onPause: wrapEvent(props.onPause, onPause), onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange), onDurationChange: wrapEvent(props.onDurationChange, onDurationChange), onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate), onProgress: wrapEvent(props.onProgress, onProgress), }); } else { element = React.createElement(tag, { controls: false, ...props, ref, onPlay: wrapEvent(props.onPlay, onPlay), onPlaying: wrapEvent(props.onPlaying, onPlaying), onWaiting: wrapEvent(props.onWaiting, onWaiting), onPause: wrapEvent(props.onPause, onPause), onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange), onDurationChange: wrapEvent(props.onDurationChange, onDurationChange), onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate), onProgress: wrapEvent(props.onProgress, onProgress), } as any); // TODO: fix this typing. }
- 如果提供了元素,则克隆并扩展其属性。
- 如果没有提供元素,则创建新的媒体元素。
-
控制方法
const controls = { play: () => { /* ... */ }, pause: () => { /* ... */ }, seek: (time: number) => { /* ... */ }, volume: (volume: number) => { /* ... */ }, mute: () => { /* ... */ }, unmute: () => { /* ... */ }, };
controls
对象提供了对媒体元素进行播放、暂停、音量调整等操作的方法。play
和pause
方法处理Promise
,确保不会重复调用play
方法。seek
方法调整播放时间。volume
、mute
和unmute
方法调整音量和静音状态。
-
副作用
useEffect(() => { const el = ref.current!; if (!el) { // Handle error return; } setState({ volume: el.volume, muted: el.muted, paused: el.paused, }); if (props.autoPlay && el.paused) { controls.play(); } }, [props.src]);
- 在组件挂载时,设置初始状态并根据
props.autoPlay
自动播放媒体。
- 在组件挂载时,设置初始状态并根据
总结
createHTMLMediaHook
: 用于创建一个自定义 Hook 来处理<audio>
或<video>
元素,封装了媒体元素的控制和状态管理。- 事件处理: 内部事件处理函数更新 Hook 状态,以反映媒体元素的当前状态。
controls
: 提供了用于播放、暂停、调整音量等功能的方法。- 副作用: 通过
useEffect
确保在媒体源更改时更新状态,并根据autoPlay
属性自动播放。
这个 Hook 提供了一个简洁的 API 来处理媒体元素的常见操作,使得在 React 组件中操作音视频元素变得更加方便和一致。