WHAT - 通过 react-use 源码学习 React(UI 篇)

简介: WHAT - 通过 react-use 源码学习 React(UI 篇)

一、官方介绍
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 (


{audio}
{JSON.stringify(state, null, 2)}

















);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
源码

import createHTMLMediaHook from './factory/createHTMLMediaHook';

const useAudio = createHTMLMediaHook('audio');
export default useAudio;
1
2
3
4
//./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,
React.VideoHTMLAttributes {
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;
pause: () => void;
mute: () => void;
unmute: () => void;
volume: (volume: number) => void;
seek: (time: number) => void;
}

type MediaPropsWithRef = HTMLMediaProps & { ref?: React.MutableRefObject };

export default function createHTMLMediaHook(
tag: 'audio' | 'video'
) {
return (elOrProps: HTMLMediaProps | React.ReactElement) => {
let element: React.ReactElement> | undefined;
let props: MediaPropsWithRef;

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;

};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
解释

createHTMLMediaHook 是一个高阶函数,用于创建 React 自定义 Hook,简化了对 和 元素的控制。它结合了 React 的状态管理和生命周期钩子来提供一个便捷的接口,用于处理 HTML 媒体元素的播放、暂停、音量控制等操作。以下是对 createHTMLMediaHook 函数的详细解析。

createHTMLMediaHook 函数解析
功能
createHTMLMediaHook 主要用于创建处理 HTML 媒体元素(如 和 )的 Hook。这个 Hook 封装了对媒体元素的常见操作和状态管理,提供了一个统一的接口来操作和控制媒体元素。

参数
tag:
类型: 'audio' | 'video'
说明: 指定要创建的 HTML 媒体元素类型,可以是 'audio' 或 'video'。
返回值
返回一个函数,该函数接受两种可能的参数:
elOrProps:
类型: HTMLMediaProps | React.ReactElement
说明: 可以是媒体元素的属性对象,也可以是包含媒体属性的 React 元素。
返回值是一个元组 [element, state, controls, ref]:
element: 渲染的 React 元素( 或 )。
state: 当前的媒体状态(HTMLMediaState)。
controls: 控制媒体播放的函数(HTMLMediaControls)。
ref: 对应媒体元素的 ref。
实现细节
参数处理

let element: React.ReactElement> | undefined;
let props: MediaPropsWithRef;

if (React.isValidElement(elOrProps)) {
element = elOrProps;
props = element.props;
} else {
props = elOrProps;
}
1
2
3
4
5
6
7
8
9
如果 elOrProps 是一个 React 元素,则提取其属性。
否则,elOrProps 被认为是直接的媒体属性。
状态和引用

const [state, setState] = useSetState({
buffered: [],
time: 0,
duration: 0,
paused: true,
muted: false,
volume: 1,
playing: false,
});
const ref = useRef(null);
1
2
3
4
5
6
7
8
9
10
使用 useSetState 管理媒体状态。有关 useSetState 具体解释可以阅读 WHAT - 通过 react-use 源码学习 React(State 篇)
ref 是一个 useRef,用于引用实际的媒体元素。
事件处理

const wrapEvent = (userEvent, proxyEvent?) => {
return (event) => {
try {
proxyEvent && proxyEvent(event);
} finally {
userEvent && userEvent(event);
}
};
};
1
2
3
4
5
6
7
8
9
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.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
如果提供了元素,则克隆并扩展其属性。
如果没有提供元素,则创建新的媒体元素。
控制方法

const controls = {
play: () => { / ... / },
pause: () => { / ... / },
seek: (time: number) => { / ... / },
volume: (volume: number) => { / ... / },
mute: () => { / ... / },
unmute: () => { / ... / },
};
1
2
3
4
5
6
7
8
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]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在组件挂载时,设置初始状态并根据 props.autoPlay 自动播放媒体。
总结
createHTMLMediaHook: 用于创建一个自定义 Hook 来处理 或 元素,封装了媒体元素的控制和状态管理。
事件处理: 内部事件处理函数更新 Hook 状态,以反映媒体元素的当前状态。
controls: 提供了用于播放、暂停、调整音量等功能的方法。
副作用: 通过 useEffect 确保在媒体源更改时更新状态,并根据 autoPlay 属性自动播放。

相关文章
|
6月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
360 0
|
1月前
|
前端开发 数据可视化 JavaScript
🚀打造卓越 UI:2024 年不容错过的 9 个 React UI 组件库✨
本文介绍了2024年最受欢迎的9个React UI组件库,每一个都在设计、功能和定制化上有独特的优势,包括Material UI、Ant Design、Chakra UI等。这些组件库为开发者提供了强大、灵活的工具,可以帮助构建现代化、无障碍且高效的Web应用程序。文章详细分析了每个库的特点、适用场景以及关键功能,帮助开发者在项目中做出最合适的选择,无论是打造企业级仪表板还是时尚的用户界面。
69 6
🚀打造卓越 UI:2024 年不容错过的 9 个 React UI 组件库✨
|
2月前
|
前端开发 JavaScript
React技术栈-React UI之ant-design使用入门
关于React技术栈中使用ant-design库的入门教程,包括了创建React应用、搭建开发环境、配置按需加载、编写和运行代码的步骤,以及遇到错误的解决方法。
38 2
React技术栈-React UI之ant-design使用入门
|
3月前
|
自然语言处理 前端开发 JavaScript
魔改react-calendar还原UI设计中的打卡日历效果
魔改react-calendar还原UI设计中的打卡日历效果
44 0
|
6月前
|
前端开发 JavaScript
详解React:Props构建可复用UI的基石
详解React:Props构建可复用UI的基石
39 0
|
资源调度 前端开发 JavaScript
关于react-admin+material ui项目的总结
关于react-admin+material ui项目的总结
163 0
|
前端开发
前端项目实战壹佰叁拾react-admin+material ui-react-admin之SelectColumnsButton之使用
前端项目实战壹佰叁拾react-admin+material ui-react-admin之SelectColumnsButton之使用
55 0
|
前端开发 数据库
前端项目实战伍拾壹​react-admin+material ui-踩坑-创建数据库完数据库表需要重启
前端项目实战伍拾壹​react-admin+material ui-踩坑-创建数据库完数据库表需要重启
83 0
|
前端开发 JavaScript
组件与Props:React中构建可复用UI的基石
组件与Props:React中构建可复用UI的基石
81 0
|
14天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
45 9