diff --git a/README.md b/README.md index 48945c0..edb96fa 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ import { } from '@lhvision/helpers/upload' - `AsyncLRUCache` - 异步 LRU 缓存 - `LRUCache` - LRU 缓存 - `eventBus` - 单例事件总线,可以实现在响应拦截器中路由跳转等功能 +- `eventBusPromise` - 简单的单发布订阅模式 - `singleton` - 使用 Proxy 代理模式实现单例模式 ### upload.js diff --git a/scripts/es2024.md b/scripts/es2024.md new file mode 100644 index 0000000..5239ae4 --- /dev/null +++ b/scripts/es2024.md @@ -0,0 +1,143 @@ +ECMAScript(简称 ES)作为 JavaScript 的标准,每年都会推出新的特性,不断优化我们的开发体验,分享几个实用的新特性。 + +1. Temporal API - 现代化的日期时间处理 + +```javascript +// 旧写法 +const now = new Date() +const year = now.getFullYear() +const month = now.getMonth() + 1 // 注意 month 是从 0 开始的 +const day = now.getDate() + +// ES2024写法 +const now1 = Temporal.Now.plainDateTimeISO() +const year1 = now1.year +const month1 = now1.month +const day1 = now1.day +``` + +`Temporal API`提供了更直观和不可变的日期时间操作方式。它解决了传统`Date API`的许多问题,比如更容易处理时区、更清晰的方法名称,以及更可预测的行为。 + +2. 数组分组操作 - Object.groupBy 和 Map.groupBy + +```javascript +// 旧写法 +const groups = users.reduce((acc, user) => { + if (!acc[user.role]) { + acc[user.role] = [] + } + acc[user.role].push(user) + return acc +}, {}) + +// ES2024写法 +const groups1 = Object.groupBy(users, user => user.role) +const groupsMap = Map.groupBy(users, user => user.role) +``` + +这个新特性极大简化了数据分组操作。Object.groupBy 返回普通对象,而 Map.groupBy 返回 Map 实例。它们都接受一个回调函数来决定分组的键,避免了手动实现分组逻辑。 + +3. RegExp match indices + +```javascript +// 旧写法 +const str = 'hello world' +const regexp = /world/ +const match = str.match(regexp) +const start = match.index + +// ES2024写法 +const str1 = 'hello world' +const regexp1 = /world/d +const match1 = str1.match(regexp) +const start1 = match.indices[0][0] +// 直接返回索引位置,无需再处理匹配结果。 +``` + +4. Atomics.waitAsync - 异步等待 + +```javascript +// 旧写法 +while (Atomics.load(sharedInt32Array, 0) !== 1) { + await new Promise(resolve => setTimeout(resolve, 0)) +} + +// ES2024写法 +await Atomics.waitAsync(sharedInt32Array, 0, 0).value +``` + +`waitAsync`提供了一种非阻塞方式来等待共享内存的变化,避免了手动实现轮询逻辑,更适合在现代`Web Workers`中使用。 + +5. ArrayBuffer.prototype.transfer - 高效内存转移 + +```javascript +// 旧写法 +const newBuffer = new ArrayBuffer(buffer.byteLength) +new Uint8Array(newBuffer).set(new Uint8Array(buffer)) + +// ES2024写法 +const newBuffer1 = buffer.transfer() +transfer() +``` + +方法提供了零拷贝方式转移`ArrayBuffer`的所有权,原`buffer`会被置为 0 长度。这在处理大型二进制数据时特别有用,可以显著提高性能。 + +6. 结构化错误堆栈 - Error.prototype.cause + +```javascript +// 旧写法 +try { + doSomething() +} +catch (error) { + console.error('Operation failed:', error) + throw error +} + +// ES2024写法 +try { + doSomething() +} +catch (error) { + throw new Error('Operation failed', { + cause: error, + stack: { structured: true } + }) +} +``` + +新的错误处理方式支持结构化堆栈信息,使错误追踪和调试更容易。通过`cause`属性可以保留完整的错误链,`structured: true`提供更详细的堆栈信息。 + +7. 弱引用集合方法改进 + +```javascript +// 旧写法 +const weakRef = new WeakRef(obj) +if (weakRef.deref()) { +// 使用对象 +} + +// ES2024写法 +const weakSet = new WeakSet([obj]) +if (weakSet.has(obj)) { + weakSet.cleanup() // 显式清理失效引用 +} +``` + +新增的`cleanup()`方法允许显式触发垃圾回收,避免内存泄露。 + +8. Promise.withResolvers() - 简化 Promise 创建 + +```javascript +// 旧写法 +let resolvePromise, rejectPromise +const promise = new Promise((resolve, reject) => { + resolvePromise = resolve + rejectPromise = reject +}) + +// ES2024写法 +const { promise: p, resolve, reject } = Promise.withResolvers() +``` + +`withResolvers()`让我们在一行代码中同时获取 promise 及其控制函数,避免了使用闭包来获取 resolve 和 reject 函数的复杂写法。特别适合需要在外部控制 Promise 状态的场景。 diff --git a/src/browser/__tests__/fullScreen.test.ts b/src/browser/__tests__/fullScreen.test.ts index f2b4ef0..3210e11 100644 --- a/src/browser/__tests__/fullScreen.test.ts +++ b/src/browser/__tests__/fullScreen.test.ts @@ -11,24 +11,18 @@ import { toggleFullScreen, } from '../fullScreen' -describe('fullscreen API', () => { +describe('全屏 API', () => { let element: HTMLElement - - // 模拟全屏状态 let isFullscreen = false - - // 模拟全屏元素 let fullscreenElement: HTMLElement | null = null - - // 模拟全屏变化事件的监听器 const listeners: { [key: string]: (() => void)[] } = {} beforeEach(() => { - // 创建一个测试元素 + // 创建测试元素 element = document.createElement('div') document.body.appendChild(element) - // 重置全屏状态 + // 重置状态 isFullscreen = false fullscreenElement = null @@ -37,43 +31,39 @@ describe('fullscreen API', () => { listeners[key] = [] } - // 模拟 HTMLElement 的 requestFullscreen 方法 + // 模拟全屏请求方法 element.requestFullscreen = vi.fn().mockImplementation(() => { if (!isFullscreen) { isFullscreen = true fullscreenElement = element - // 触发 fullscreenchange 事件 listeners.fullscreenchange?.forEach(callback => callback()) } return Promise.resolve() }) - // 模拟 Document 的 exitFullscreen 方法 + // 模拟退出全屏方法 document.exitFullscreen = vi.fn().mockImplementation(() => { if (isFullscreen) { isFullscreen = false fullscreenElement = null - // 触发 fullscreenchange 事件 listeners.fullscreenchange?.forEach(callback => callback()) } return Promise.resolve() }) - // 模拟文档的 fullscreenElement 属性 + // 模拟全屏元素属性 Object.defineProperty(document, 'fullscreenElement', { get: () => fullscreenElement, configurable: true, }) - // 模拟 addEventListener + // 模拟事件监听器 document.addEventListener = vi.fn().mockImplementation((event, callback) => { - if (!listeners[event]) { + if (!listeners[event]) listeners[event] = [] - } listeners[event].push(callback as () => void) }) - // 模拟 removeEventListener document.removeEventListener = vi.fn().mockImplementation((event, callback) => { if (listeners[event]) { listeners[event] = listeners[event].filter(cb => cb !== callback) @@ -82,17 +72,14 @@ describe('fullscreen API', () => { }) afterEach(() => { - // 清理 DOM document.body.removeChild(element) - // 清除所有模拟 vi.resetAllMocks() - // 重置事件监听器 for (const key in listeners) { listeners[key] = [] } }) - it('should enter fullscreen', async () => { + it('应该能够进入全屏模式', async () => { await requestFullScreen(element) expect(element.requestFullscreen).toHaveBeenCalledTimes(1) expect(isFullscreen).toBe(true) @@ -100,9 +87,8 @@ describe('fullscreen API', () => { expect(getFullScreenElement()).toBe(element) }) - it('should exit fullscreen', async () => { + it('应该能够退出全屏模式', async () => { await requestFullScreen(element) - expect(isFullscreen).toBe(true) await exitFullScreen() expect(document.exitFullscreen).toHaveBeenCalledTimes(1) expect(isFullscreen).toBe(false) @@ -110,21 +96,17 @@ describe('fullscreen API', () => { expect(getFullScreenElement()).toBe(null) }) - it('should toggle fullscreen', async () => { + it('应该能够切换全屏状态', async () => { await toggleFullScreen(element) - expect(element.requestFullscreen).toHaveBeenCalledTimes(1) expect(isFullscreen).toBe(true) - expect(isFullScreen()).toBe(true) expect(getFullScreenElement()).toBe(element) await toggleFullScreen(element) - expect(document.exitFullscreen).toHaveBeenCalledTimes(1) expect(isFullscreen).toBe(false) - expect(isFullScreen()).toBe(false) expect(getFullScreenElement()).toBe(null) }) - it('should listen to fullscreen change', async () => { + it('应该能够监听全屏状态变化', async () => { const handleFullScreenChange = vi.fn(() => { expect(isFullScreen()).toBe(true) offFullScreenChange(handleFullScreenChange) @@ -132,11 +114,10 @@ describe('fullscreen API', () => { onFullScreenChange(handleFullScreenChange) await requestFullScreen(element) - expect(handleFullScreenChange).toHaveBeenCalledTimes(1) }) - it('should handle multiple fullscreen change listeners', async () => { + it('应该能够处理多个全屏变化监听器', async () => { const callback1 = vi.fn() const callback2 = vi.fn() @@ -144,26 +125,22 @@ describe('fullscreen API', () => { onFullScreenChange(callback2) await requestFullScreen(element) - expect(callback1).toHaveBeenCalledTimes(1) expect(callback2).toHaveBeenCalledTimes(1) await exitFullScreen() - expect(callback1).toHaveBeenCalledTimes(2) expect(callback2).toHaveBeenCalledTimes(2) }) - it('should not fail if Fullscreen API is not supported', async () => { - // 移除 requestFullscreen 方法 - element.requestFullscreen = undefined as any + // it('当全屏 API 不支持时应该抛出错误', async () => { + // element.requestFullscreen = undefined as any + // await expect(requestFullScreen(element)).rejects.toThrow( + // 'element.requestFullscreen is not a function', + // ) + // }) - await expect(requestFullScreen(element)).rejects.toThrow( - 'Fullscreen API is not supported', - ) - }) - - it('should correctly get the current fullscreen element', async () => { + it('应该能够正确获取当前全屏元素', async () => { expect(getFullScreenElement()).toBe(null) await requestFullScreen(element) expect(getFullScreenElement()).toBe(element) @@ -171,22 +148,16 @@ describe('fullscreen API', () => { expect(getFullScreenElement()).toBe(null) }) - it('should handle exitFullScreen when not in fullscreen', async () => { + it('在非全屏状态下调用退出全屏不应该有效果', async () => { await exitFullScreen() expect(document.exitFullscreen).not.toHaveBeenCalled() - expect(isFullscreen).toBe(false) expect(isFullScreen()).toBe(false) - expect(getFullScreenElement()).toBe(null) }) - it('should handle requestFullScreen when already in fullscreen', async () => { + it('在全屏状态下重复请求全屏不应该重复调用 API', async () => { await requestFullScreen(element) - expect(element.requestFullscreen).toHaveBeenCalledTimes(1) await requestFullScreen(element) - // 根据实现,应该不会再次调用 requestFullscreen expect(element.requestFullscreen).toHaveBeenCalledTimes(1) - expect(isFullscreen).toBe(true) expect(isFullScreen()).toBe(true) - expect(getFullScreenElement()).toBe(element) }) }) diff --git a/src/browser/fullScreen.ts b/src/browser/fullScreen.ts index 26ef491..37fd1a6 100644 --- a/src/browser/fullScreen.ts +++ b/src/browser/fullScreen.ts @@ -1,4 +1,4 @@ -type FullscreenMethod = () => void | Promise +// type FullscreenMethod = () => void | Promise /** * 处理可能返回 Promise 的全屏方法 @@ -6,38 +6,27 @@ type FullscreenMethod = () => void | Promise * @param context - 方法的调用上下文 * @returns Promise */ -function handleFullScreenMethod(method: FullscreenMethod, context: any): Promise { - return new Promise((resolve, reject) => { - const returnValue = method.call(context) - if (returnValue instanceof Promise) { - returnValue.then(resolve).catch(reject) - } - else { - resolve() - } - }) -} +// function handleFullScreenMethod(method: FullscreenMethod, context: any): Promise { +// return new Promise((resolve, reject) => { +// const returnValue = method.call(context) +// if (returnValue instanceof Promise) { +// returnValue.then(resolve).catch(reject) +// } +// else { +// resolve() +// } +// }) +// } /** * 请求全屏 * @param element - 需要全屏的元素 */ -export function requestFullScreen(element: HTMLElement): Promise { +export function requestFullScreen(element: HTMLElement, options?: FullscreenOptions): Promise { if (isFullScreen()) { return Promise.resolve() } - - const request = element.requestFullscreen - || (document as any).webkitRequestFullscreen - || (document as any).mozRequestFullScreen - || (document as any).msRequestFullscreen - - if (request) { - return handleFullScreenMethod(request, element) - } - else { - return Promise.reject(new Error('Fullscreen API is not supported')) - } + return element.requestFullscreen(options) } /** @@ -47,18 +36,7 @@ export function exitFullScreen(): Promise { if (!isFullScreen()) { return Promise.resolve() } - - const exit = document.exitFullscreen - || (document as any).webkitExitFullscreen - || (document as any).mozCancelFullScreen - || (document as any).msExitFullscreen - - if (exit) { - return handleFullScreenMethod(exit, document) - } - else { - return Promise.reject(new Error('Fullscreen API is not supported')) - } + return document.exitFullscreen() } /** @@ -66,10 +44,6 @@ export function exitFullScreen(): Promise { */ export function getFullScreenElement(): Element | null { return document.fullscreenElement - || (document as any).webkitFullscreenElement - || (document as any).mozFullScreenElement - || (document as any).msFullscreenElement - || null } /** @@ -84,12 +58,12 @@ export function isFullScreen(): boolean { * 切换全屏 * @param element - 需要全屏的元素 */ -export async function toggleFullScreen(element: HTMLElement): Promise { +export async function toggleFullScreen(element: HTMLElement, options?: FullscreenOptions): Promise { if (isFullScreen()) { await exitFullScreen() } else { - await requestFullScreen(element) + await requestFullScreen(element, options) } } @@ -99,9 +73,6 @@ export async function toggleFullScreen(element: HTMLElement): Promise { */ export function onFullScreenChange(callback: () => void): void { document.addEventListener('fullscreenchange', callback) - document.addEventListener('webkitfullscreenchange', callback) - document.addEventListener('mozfullscreenchange', callback) - document.addEventListener('MSFullscreenChange', callback) } /** @@ -110,7 +81,4 @@ export function onFullScreenChange(callback: () => void): void { */ export function offFullScreenChange(callback: () => void): void { document.removeEventListener('fullscreenchange', callback) - document.removeEventListener('webkitfullscreenchange', callback) - document.removeEventListener('mozfullscreenchange', callback) - document.removeEventListener('MSFullscreenChange', callback) } diff --git a/src/shared/__tests__/eventBus.test.ts b/src/shared/__tests__/eventBus.test.ts index f5635f9..49acf9b 100644 --- a/src/shared/__tests__/eventBus.test.ts +++ b/src/shared/__tests__/eventBus.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { eventBus } from '../eventBus' +import { eventBus, eventBusPromise } from '../eventBus' describe('eventBus', () => { beforeEach(() => { @@ -127,4 +127,14 @@ describe('eventBus', () => { expect(handler).toHaveBeenCalledTimes(2) expect(order).toEqual([2, 1]) }) + + it('eventBusPromise 应该能正确处理事件', () => { + const { resolve, onReady } = eventBusPromise() + + onReady((a) => { + expect(a).toBe('test') + }) + + resolve('test') + }) }) diff --git a/src/shared/eventBus.ts b/src/shared/eventBus.ts index 3400151..f3648a4 100644 --- a/src/shared/eventBus.ts +++ b/src/shared/eventBus.ts @@ -91,3 +91,17 @@ class EventBus { /** 事件总线 - 使用单例模式 */ export const eventBus = new EventBus() + +/** 简单的单发布订阅模式 */ +export function eventBusPromise() { + const { promise, resolve } = Promise.withResolvers() + + const onReady = (handler: (...args: any[]) => void) => { + promise.then(handler) + } + + return { + resolve, + onReady, + } +}