@complex-suite/utils
Advanced tools
+36
-0
@@ -0,1 +1,37 @@ | ||
| <!-- | ||
| 版本规则: | ||
| - 顶部 `### current` 记录当前未发版的改动(可能为空)。 | ||
| - 版本升级发版时,将 `current` 更替为本次发版版本号(如 `### 3.0.12`),并在其上方新建一个空的 `### current`。 | ||
| - 这样每次发版后,顶部始终保留一个 `### current`(可能为空)用于记录后续改动。 | ||
| --> | ||
| ### current | ||
| ### 3.0.15 | ||
| - fix(observe): `observe` 类型守卫改为 `type !== 'object' && type !== 'array'`,数组可正常进入 Observer。 | ||
| - fix(type): `isSame` 新增 Map/Set 比较分支:Map 比较 size 后逐 key 递归比较 value;Set 比较 size 后逐元素递归比较。 | ||
| - fix(type): `isEmptyObject` 改用 `getComplexType`,map/set 用 size===0 判断,date/regExp/file/blob 返回 false。 | ||
| - fix(object): `deepCloneData` 补充 date 分支 `new Date(data)` 和 regExp 分支 `new RegExp(data.source, data.flags)`。 | ||
| - fix(color): `parseColor` 无效颜色值在 `exportMsg` 后显式 `return { r: 0, g: 0, b: 0, a: 1 }`,保证始终返回合法 RGBA 对象。 | ||
| - fix(string): `getRandomInList` 返回类型改为 `T | undefined`,空列表直接 return undefined;`getRandomLetter` 在 list 为空时 return '' 兜底。 | ||
| - fix(function): `triggerPromise` 将 `.then(success, fail)` 拆为 `.then(success).catch(fail)`,catch 同时捕获 rejection 和 success 内部异常。 | ||
| - fix(utils): `openAnchor` 解析文件名时去除查询参数和 hash:`url.split('/').pop()?.split('?')[0]?.split('#')[0] || 'download'`。 | ||
| - fix(observe): `Watcher.run` 条件改为 `value !== this.value || (typeof value == 'object' && value !== null)`,排除 null 值误触发。 | ||
| - fix(type): `isSame` 增加 Date/RegExp 复杂对象的比较,Object.keys 无法正确反映它们的值。 | ||
| - fix(worker): `startWorker` 在 onerror 和 onmessage 处理后增加 `worker.terminate()`,避免 Worker 线程累积泄漏。 | ||
| - fix(observe): `traverse` 用 try/finally 包裹,确保异常路径下 seenObjects 也被清理,避免污染下次调用导致依赖收集失效。 | ||
| - fix(object): `deepCloneData` 对 Map 的 key 进行深拷贝(`result.set(deepCloneData(key, map), deepCloneData(value, map))`),与 Set 元素处理保持一致,基本类型直接返回原值无性能影响。 | ||
| - fix(observe): `defineReactive` 浅拷贝调用方传入的 descriptor(`{ ...(option.descriptor || {}) }`),避免污染调用方对象。 | ||
| - fix(color): `parseColor` 在十六进制颜色长度不为 6 时调用 `exportMsg` 提示,无效格式不再静默返回 undefined。 | ||
| - fix(wait): `Wait.trigger` 用 try/finally 包裹 forEach,保证回调抛错时 destroy 仍执行,避免实例泄漏。 | ||
| - perf(tree): `formatTree.parse` 将扩展运算符改为 for 循环逐个 push,避免大数组栈溢出,结果与原逻辑一致。 | ||
| - fix(number): `parseNum` 检测科学计数法(含 'e')时用 `toLocaleString('fullwide', { useGrouping: false })` 展开,修正极小/极大数解析错误。注:IE11 不支持 'fullwide' 会静默回退,但项目仅兼容 Android 16+ 不受影响。 | ||
| - fix(object): `jsonToForm` 按类型分支处理 FormData.append:object/array→JSON.stringify,file/blob→直接 append,null/undefined→跳过不 append,其余→String() 转换,避免 number/boolean 抛错。 | ||
| - fix(utils): `$exportMsg` 前缀由 `[complex-plugin]` 改为 `[complex-suite/utils]`,与包名一致。 | ||
| - fix(utils): `openAnchor` 下载文件名提取增加空字符串回退 `url.split('/').pop() || 'download'`,URL 以 `/` 结尾时不再为空。 | ||
| - perf(object): `deepCloneData` 普通对象改用 `Object.create(Object.getPrototypeOf(data))` 创建时指定原型,避免 `setPrototypeOf` 导致隐藏类失效;数组仍用 `[]` 创建(Object.create(Array.prototype) 无 length 内部槽),仅在原型不同时才 setPrototypeOf。 | ||
| - fix(storage): `storage` 模块用 `typeof localStorage !== 'undefined'` 守卫,`_build*` 方法增加 `!targetStorage` 短路返回,SSR 环境(Node.js)中不再抛 ReferenceError。 | ||
| - fix(string): `camelToLine` 正则改为 `/(?!^)([A-Z])/g`,首字母大写不插入前导分隔符(如 `FirstName` → `first_name` 而非 `_first_name`)。 | ||
| - fix(utils): `getQueryData` 中 `split('=')` 改为 `indexOf('=')` + `slice`,只切第一个等号,含等号的值(如 base64)不再被截断。decodeURIComponent 跳过(用户确认不处理)。 | ||
| ### 3.0.11 | ||
@@ -2,0 +38,0 @@ - chore: 小版本升级,保持与套件其他子包版本同步。 |
+1
-1
| { | ||
| "name": "@complex-suite/utils", | ||
| "version": "3.0.14", | ||
| "version": "3.0.15", | ||
| "description": "a complex utils", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -51,6 +51,10 @@ | ||
| trigger() { | ||
| this.list.forEach(value => { | ||
| value() | ||
| }) | ||
| this.destroy() | ||
| // try/finally 包裹,保证回调抛错时 destroy 仍执行,避免 Wait 实例泄漏 | ||
| try { | ||
| this.list.forEach(value => { | ||
| value() | ||
| }) | ||
| } finally { | ||
| this.destroy() | ||
| } | ||
| } | ||
@@ -57,0 +61,0 @@ destroy() { |
@@ -39,3 +39,4 @@ import $exportMsg from '../utils/$exportMsg' | ||
| triggerFunction(success, res) | ||
| }, err => { | ||
| }).catch(err => { | ||
| // 同时捕获 promise rejection 和 success 回调内部异常 | ||
| triggerFunction(fail, err) | ||
@@ -42,0 +43,0 @@ }).finally(() => { |
@@ -13,3 +13,8 @@ | ||
| // 将数字转换为字符串,避免浮点数精度问题 | ||
| const numStr = num.toString() | ||
| // 注意:num.toString() 对极小/极大数字会生成科学计数法(如 1e-7 → "1e-7"),导致 parseInt('1e-7') 返回 1(错误,应为 0) | ||
| // 因此检测科学计数法时用 toLocaleString('fullwide') 展开(不产生精度尾巴) | ||
| let numStr = num.toString() | ||
| if (numStr.includes('e')) { | ||
| numStr = num.toLocaleString('fullwide', { useGrouping: false }) | ||
| } | ||
| const [integerStr, decimalStr] = numStr.split('.') | ||
@@ -16,0 +21,0 @@ const integer = parseInt(integerStr) || 0 |
@@ -18,5 +18,15 @@ import getComplexType from './../type/getComplexType' | ||
| } else { | ||
| result = type === 'object' ? {} : [] | ||
| if (Object.getPrototypeOf(result) !== Object.getPrototypeOf(data)) { | ||
| Object.setPrototypeOf(result, Object.getPrototypeOf(data)) | ||
| // 用 Object.create 创建时就指定原型,避免 setPrototypeOf 导致 V8 隐藏类失效(性能下降) | ||
| // Object.create(Object.getPrototypeOf(data)) 与原 {} / [] 行为一致: | ||
| // - 对于普通对象,Object.getPrototypeOf({}) === Object.prototype,Object.create(Object.prototype) 等价于 {} | ||
| // - 对于数组,Object.getPrototypeOf([]) === Array.prototype,Object.create(Array.prototype) 不会自动初始化 length 等内部槽 | ||
| // 因此数组仍需用 [] 创建后再设置原型(Object.create(Array.prototype) 不是真正的数组) | ||
| if (type === 'array') { | ||
| result = [] | ||
| if (Object.getPrototypeOf(result) !== Object.getPrototypeOf(data)) { | ||
| Object.setPrototypeOf(result, Object.getPrototypeOf(data)) | ||
| } | ||
| } else { | ||
| // 普通对象直接用 Object.create 创建,避免后续 setPrototypeOf | ||
| result = Object.create(Object.getPrototypeOf(data)) | ||
| } | ||
@@ -35,3 +45,4 @@ map.set(data, result) | ||
| (data as unknown as Map<any, any>).forEach((value, key) => { | ||
| result.set(key, deepCloneData(value, map)) | ||
| // key 同样需要深拷贝,与 Set 元素处理保持一致;基本类型 deepCloneData 直接返回原值,无性能影响 | ||
| result.set(deepCloneData(key, map), deepCloneData(value, map)) | ||
| }) | ||
@@ -46,2 +57,7 @@ return result as unknown as T | ||
| return result as unknown as T | ||
| } else if (type === 'date') { | ||
| return new Date(data as unknown as Date) as unknown as T | ||
| } else if (type === 'regExp') { | ||
| const d = data as unknown as RegExp | ||
| return new RegExp(d.source, d.flags) as unknown as T | ||
| } else { | ||
@@ -48,0 +64,0 @@ return data |
@@ -89,4 +89,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| if (!item.load && item.data[this.childrenProp]) { | ||
| // 使用 push 和扩展运算符替换 concat,可以避免重复创建新数组,性能更好。 | ||
| list.push(...item.data[this.childrenProp]) | ||
| // 使用 for 循环逐个 push,避免扩展运算符对大数组触发参数数量上限导致的栈溢出 | ||
| const children = item.data[this.childrenProp] | ||
| for (let i = 0; i < children.length; i++) { | ||
| list.push(children[i]) | ||
| } | ||
| } | ||
@@ -93,0 +96,0 @@ }) |
@@ -13,6 +13,11 @@ import getComplexType from './../type/getComplexType' | ||
| const type = getComplexType(jsonData[prop]) | ||
| if (type === 'object') { | ||
| if (type === 'object' || type === 'array') { | ||
| formData.append(prop, JSON.stringify(jsonData[prop])) | ||
| } else if (type === 'file' || type === 'blob') { | ||
| formData.append(prop, jsonData[prop]) | ||
| } else if (type === 'null' || type === 'undefined') { | ||
| // null/undefined 跳过不 append | ||
| continue | ||
| } else { | ||
| formData.append(prop, jsonData[prop]) | ||
| formData.append(prop, String(jsonData[prop])) | ||
| } | ||
@@ -19,0 +24,0 @@ } |
@@ -10,4 +10,8 @@ import { observeProp, observeObject } from "./Observer" | ||
| function traverse(val: unknown) { | ||
| _traverse(val, seenObjects) | ||
| seenObjects.clear() | ||
| try { | ||
| _traverse(val, seenObjects) | ||
| } finally { | ||
| // 确保异常路径下 seenObjects 也被清理,避免污染下次调用导致依赖收集失效 | ||
| seenObjects.clear() | ||
| } | ||
| } | ||
@@ -14,0 +18,0 @@ |
@@ -20,3 +20,4 @@ import getComplexType from "../type/getComplexType" | ||
| function observe(value: unknown) { | ||
| if (getComplexType(value) !== 'object') { | ||
| const type = getComplexType(value) | ||
| if (type !== 'object' && type !== 'array') { | ||
| return | ||
@@ -23,0 +24,0 @@ } |
@@ -128,3 +128,3 @@ import getType from '../type/getType' | ||
| const value = this.get() | ||
| if (value !== this.value || typeof value == 'object') { | ||
| if (value !== this.value || (typeof value == 'object' && value !== null)) { | ||
| const oldValue = this.value | ||
@@ -131,0 +131,0 @@ this.value = value |
@@ -34,3 +34,4 @@ import getComplexType from '../type/getComplexType' | ||
| const setter = currentDescriptor?.set | ||
| const descriptor = option.descriptor || {} | ||
| // 浅拷贝 descriptor,避免修改调用方传入的对象(configurable/enumerable 默认值会污染原对象) | ||
| const descriptor = { ...(option.descriptor || {}) } | ||
| if (descriptor.configurable == undefined) { | ||
@@ -37,0 +38,0 @@ descriptor.configurable = true |
+28
-12
@@ -23,4 +23,7 @@ | ||
| }, | ||
| _buildSetData(targetStorage: Storage) { | ||
| _buildSetData(targetStorage: Storage | undefined) { | ||
| return function(name: string, value?: unknown, time?: number) { | ||
| if (!targetStorage) { | ||
| return false | ||
| } | ||
| const prop = storage.getProp(name) | ||
@@ -41,4 +44,7 @@ const timeProp = storage.getTimeProp(name) | ||
| }, | ||
| _buildGetData(targetStorage: Storage) { | ||
| _buildGetData(targetStorage: Storage | undefined) { | ||
| return function(name: string, option?: true | number, refresh?: boolean) { | ||
| if (!targetStorage) { | ||
| return undefined | ||
| } | ||
| const prop = storage.getProp(name) | ||
@@ -77,4 +83,7 @@ const timeProp = storage.getTimeProp(name) | ||
| }, | ||
| _buildRemoveData(targetStorage: Storage) { | ||
| _buildRemoveData(targetStorage: Storage | undefined) { | ||
| return function(name: string) { | ||
| if (!targetStorage) { | ||
| return | ||
| } | ||
| const prop = storage.getProp(name) | ||
@@ -86,4 +95,7 @@ const timeProp = storage.getTimeProp(name) | ||
| }, | ||
| _buildClearData(targetStorage: Storage) { | ||
| _buildClearData(targetStorage: Storage | undefined) { | ||
| return function() { | ||
| if (!targetStorage) { | ||
| return | ||
| } | ||
| targetStorage.clear() | ||
@@ -102,12 +114,16 @@ } | ||
| storage.setData = storage._buildSetData(localStorage) | ||
| storage.setSessionData = storage._buildSetData(sessionStorage) | ||
| storage.getData = storage._buildGetData(localStorage) | ||
| storage.getSessionData = storage._buildGetData(sessionStorage) | ||
| storage.removeData = storage._buildRemoveData(localStorage) | ||
| storage.removeSessionData = storage._buildRemoveData(sessionStorage) | ||
| storage.clearData = storage._buildClearData(localStorage) | ||
| storage.clearSessionData = storage._buildClearData(sessionStorage) | ||
| // typeof 守卫:SSR 环境(Node.js)中 localStorage/sessionStorage 未定义,直接访问会抛 ReferenceError | ||
| const _localStorage = typeof localStorage !== 'undefined' ? localStorage : undefined | ||
| const _sessionStorage = typeof sessionStorage !== 'undefined' ? sessionStorage : undefined | ||
| storage.setData = storage._buildSetData(_localStorage) | ||
| storage.setSessionData = storage._buildSetData(_sessionStorage) | ||
| storage.getData = storage._buildGetData(_localStorage) | ||
| storage.getSessionData = storage._buildGetData(_sessionStorage) | ||
| storage.removeData = storage._buildRemoveData(_localStorage) | ||
| storage.removeSessionData = storage._buildRemoveData(_sessionStorage) | ||
| storage.clearData = storage._buildClearData(_localStorage) | ||
| storage.clearSessionData = storage._buildClearData(_sessionStorage) | ||
| export default storage | ||
@@ -17,6 +17,11 @@ import { describe, it, expect } from 'vitest' | ||
| it('should handle a string that starts with an uppercase letter', () => { | ||
| expect(camelToLine('HelloWorld')).toBe('_hello_world') | ||
| it('should handle a string that starts with an uppercase letter (no leading underscore)', () => { | ||
| expect(camelToLine('HelloWorld')).toBe('hello_world') | ||
| expect(camelToLine('XMLHttpRequest')).toBe('x_m_l_http_request') | ||
| }) | ||
| it('should use custom separator', () => { | ||
| expect(camelToLine('HelloWorld', '-')).toBe('hello-world') | ||
| }) | ||
| it('should handle an empty string', () => { | ||
@@ -23,0 +28,0 @@ expect(camelToLine('')).toBe('') |
| function camelToLine(str: string, line = '_'): string { | ||
| return str.replace(/([A-Z])/g, line + '$1').toLowerCase() | ||
| // (?!^) 排除首字符,首字母大写不插入前导分隔符(如 'FirstName' → 'first_name' 而非 '_first_name') | ||
| return str.replace(/(?!^)([A-Z])/g, line + '$1').toLowerCase() | ||
| } | ||
| export default camelToLine |
@@ -8,3 +8,4 @@ import getRandomNum from './../number/getRandomNum' | ||
| */ | ||
| function getRandomInList<T>(list: T[]):T { | ||
| function getRandomInList<T>(list: T[]): T | undefined { | ||
| if (!list.length) return undefined | ||
| return list[getRandomNum(0, list.length - 1)] | ||
@@ -11,0 +12,0 @@ } |
@@ -46,5 +46,6 @@ import getRandomInList from './getRandomInList' | ||
| } | ||
| return getRandomInList(list) | ||
| if (list.length === 0) return '' | ||
| return getRandomInList(list)! | ||
| } | ||
| export default getRandomLetter |
@@ -1,2 +0,2 @@ | ||
| import getType from './getType' | ||
| import getComplexType from './getComplexType' | ||
| import isEmptyArray from './isEmptyArray' | ||
@@ -8,3 +8,2 @@ import isEmptyObject from './isEmptyObject' | ||
| * @param {*} value 需要判断的值 | ||
| * @param {string[]} [checkList] 需要深入判断的数据类型,对象和数组可选 | ||
| * @returns {boolean} value is Empty | ||
@@ -17,4 +16,4 @@ */ | ||
| } else { | ||
| const type = getType(value) | ||
| if (type === 'object') { | ||
| const type = getComplexType(value) | ||
| if (type === 'object' || type === 'map' || type === 'set') { | ||
| return isEmptyObject(value, type) | ||
@@ -21,0 +20,0 @@ } else if (type === 'array') { |
@@ -1,2 +0,2 @@ | ||
| import getType from './getType' | ||
| import getComplexType from './getComplexType' | ||
@@ -6,3 +6,3 @@ /** | ||
| * @param {*} value 需要判断的值 | ||
| * @param {string} type 类型 | ||
| * @param {string} type 类型(getComplexType 返回值) | ||
| * @returns {boolean} value is EmptyObject | ||
@@ -12,4 +12,10 @@ */ | ||
| if (!type) { | ||
| type = getType(value) | ||
| type = getComplexType(value) | ||
| } | ||
| if (type === 'map' || type === 'set') { | ||
| return (value as Map<any, any> | Set<any>).size === 0 | ||
| } | ||
| if (type === 'date' || type === 'regExp' || type === 'file' || type === 'blob') { | ||
| return false | ||
| } | ||
| if (type === 'object') { | ||
@@ -20,7 +26,6 @@ for (const _prop in value as Record<PropertyKey, any>) { | ||
| return true | ||
| } else { | ||
| return false | ||
| } | ||
| return false | ||
| } | ||
| export default isEmptyObject |
+41
-36
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import getType from './getType' | ||
| import getComplexType from './getComplexType' | ||
| /** | ||
| * 是否相同=>对象则直接进行属性的依次对比 | ||
| * 额外处理 Date(比较 getTime)、RegExp(比较 source/flags)、Map(逐键值)、Set(逐元素) 等复杂对象 | ||
| * File/Blob 通过 Object.keys 比较属性 | ||
| * @param {*} value 需要判断的数据 | ||
| * @param {*} other 需要判断的数据 | ||
| */ | ||
| function isSame(value: any, other: any): boolean { | ||
| const type = getType(value) | ||
| const otherType = getType(other) | ||
| if (type !== otherType) { | ||
| // 类型不同直接返回不同 | ||
| return false | ||
| } else { | ||
| if (type == 'object' || type === 'array') { | ||
| // 对象或者数组则依次对比每个属性 | ||
| const propList = Object.keys(value) | ||
| const otherPropList = Object.keys(other) | ||
| if (propList.length === otherPropList.length) { | ||
| if (propList.length === 0) { | ||
| return true | ||
| } else { | ||
| for (let i = 0; i < propList.length; i++) { | ||
| const prop = propList[i] | ||
| if (otherPropList.indexOf(prop) < 0) { | ||
| return false | ||
| } | ||
| const fg = isSame(value[prop], other[prop]) | ||
| if (!fg) { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| } else { | ||
| return false | ||
| } | ||
| } else { | ||
| if (type === 'number' && isNaN(value) && isNaN(other)) { | ||
| // NAN === NAN | ||
| return true | ||
| } | ||
| return value === other | ||
| const type = getComplexType(value) | ||
| const otherType = getComplexType(other) | ||
| if (type !== otherType) return false | ||
| if (type === 'date') return value.getTime() === other.getTime() | ||
| if (type === 'regExp') return value.source === other.source && value.flags === other.flags | ||
| if (type === 'map') { | ||
| if (value.size !== other.size) return false | ||
| for (const [k, v] of value) { | ||
| if (!other.has(k) || !isSame(v, other.get(k))) return false | ||
| } | ||
| return true | ||
| } | ||
| if (type === 'set') { | ||
| if (value.size !== other.size) return false | ||
| for (const v of value) { | ||
| if (![...other].some(ov => isSame(v, ov))) return false | ||
| } | ||
| return true | ||
| } | ||
| // object / array / file / blob 通过 Object.keys 逐属性比较 | ||
| if (type === 'object' || type === 'array' || type === 'file' || type === 'blob') { | ||
| const propList = Object.keys(value) | ||
| const otherPropList = Object.keys(other) | ||
| if (propList.length !== otherPropList.length) return false | ||
| if (propList.length === 0) return true | ||
| for (let i = 0; i < propList.length; i++) { | ||
| const prop = propList[i] | ||
| if (otherPropList.indexOf(prop) < 0) return false | ||
| if (!isSame(value[prop], other[prop])) return false | ||
| } | ||
| return true | ||
| } | ||
| // 基础类型:NaN === NaN 特殊处理 | ||
| if (type === 'number' && isNaN(value) && isNaN(other)) return true | ||
| return value === other | ||
| } | ||
| export default isSame |
@@ -8,5 +8,5 @@ import exportMsg, { consoleType } from './exportMsg' | ||
| function $exportMsg(msg: string, type?: consoleType) { | ||
| exportMsg('[complex-plugin]' + msg, type) | ||
| exportMsg('[complex-suite/utils]' + msg, type) | ||
| } | ||
| export default $exportMsg |
@@ -13,4 +13,8 @@ /** | ||
| if (item) { | ||
| const [key, value] = item.split('=') | ||
| queryData[key] = value | ||
| const eqIndex = item.indexOf('=') | ||
| if (eqIndex > -1) { | ||
| queryData[item.slice(0, eqIndex)] = item.slice(eqIndex + 1) | ||
| } else { | ||
| queryData[item] = undefined | ||
| } | ||
| } | ||
@@ -17,0 +21,0 @@ } |
@@ -18,4 +18,4 @@ | ||
| if (download === true) { | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| download = url.split('/').pop()! | ||
| // url 以 / 结尾时 pop() 返回空字符串,回退到默认文件名 | ||
| download = url.split('/').pop()?.split('?')[0]?.split('#')[0] || 'download' | ||
| } | ||
@@ -22,0 +22,0 @@ anchor.setAttribute('download', download) |
@@ -25,3 +25,7 @@ import exportMsg from "./exportMsg" | ||
| return { r, g, b, a: 1 } | ||
| } else { | ||
| // 非法长度(既非3也非6),给出错误提示 | ||
| exportMsg(utilsLocale.t('color.invalidFormat')) | ||
| } | ||
| return { r: 0, g: 0, b: 0, a: 1 } | ||
| } else { | ||
@@ -46,2 +50,3 @@ // 处理 rgb 和 rgba 颜色值 | ||
| exportMsg(utilsLocale.t('color.invalidFormat')) | ||
| return { r: 0, g: 0, b: 0, a: 1 } | ||
| } | ||
@@ -48,0 +53,0 @@ } |
@@ -80,2 +80,3 @@ import getType from '../type/getType' | ||
| URL.revokeObjectURL(url) | ||
| worker.terminate() | ||
| } | ||
@@ -90,2 +91,3 @@ worker.onmessage = function (event) { | ||
| URL.revokeObjectURL(url) | ||
| worker.terminate() | ||
| } | ||
@@ -92,0 +94,0 @@ worker.postMessage({ |
225182
4.21%5010
1.58%