From 762ac3a3f56e03bb0cb66f4de7378867adfe6a76 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Mon, 25 Apr 2022 21:12:26 +0800 Subject: [PATCH] feat: use lru-cache in emoji-picker --- .../src/components/emoji-picker/index.tsx | 20 +-- packages/client/src/helpers/json.tsx | 8 + packages/client/src/helpers/lru-cache.ts | 149 ++++++++++++++++++ 3 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 packages/client/src/helpers/lru-cache.ts diff --git a/packages/client/src/components/emoji-picker/index.tsx b/packages/client/src/components/emoji-picker/index.tsx index d0aad905..f2135546 100644 --- a/packages/client/src/components/emoji-picker/index.tsx +++ b/packages/client/src/components/emoji-picker/index.tsx @@ -1,12 +1,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Popover, Typography } from '@douyinfe/semi-ui'; import { EXPRESSIONES, GESTURES, SYMBOLS, OBJECTS, ACTIVITIES, SKY_WEATHER } from './constants'; -import { setStorage, getStorage } from 'helpers/storage'; +import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { useToggle } from 'hooks/use-toggle'; import styles from './index.module.scss'; const { Title } = Typography; -const RECENT_USED_EMOJI_KEY = 'RECENT_USED_EMOJI_KEY'; + +const emojiLocalStorageLRUCache = createKeysLocalStorageLRUCache('EMOJI_PICKER', 20); const LIST = [ { @@ -49,22 +50,17 @@ export const EmojiPicker: React.FC = ({ onSelectEmoji, children }) => { const selectEmoji = useCallback( (emoji) => { - setStorage(RECENT_USED_EMOJI_KEY, [...recentUsed, emoji].join('-')); - setRecentUsed((arr) => [...arr, emoji]); + emojiLocalStorageLRUCache.put(emoji); + setRecentUsed(emojiLocalStorageLRUCache.get() as string[]); onSelectEmoji && onSelectEmoji(emoji); }, - [onSelectEmoji, recentUsed] + [onSelectEmoji] ); useEffect(() => { if (!visible) return; - try { - const recentUsed = getStorage(RECENT_USED_EMOJI_KEY); - const toArr = recentUsed.split('-'); - setRecentUsed(toArr); - } catch (e) { - // - } + emojiLocalStorageLRUCache.syncFromStorage(); + setRecentUsed(emojiLocalStorageLRUCache.get() as string[]); }, [visible]); return ( diff --git a/packages/client/src/helpers/json.tsx b/packages/client/src/helpers/json.tsx index ded039b4..665c7b36 100644 --- a/packages/client/src/helpers/json.tsx +++ b/packages/client/src/helpers/json.tsx @@ -5,3 +5,11 @@ export const safeJSONParse = (str, defaultValue = {}) => { return defaultValue; } }; + +export const safeJSONStringify = (obj, defaultValue = '{}') => { + try { + return JSON.stringify(obj); + } catch (e) { + return defaultValue; + } +}; diff --git a/packages/client/src/helpers/lru-cache.ts b/packages/client/src/helpers/lru-cache.ts new file mode 100644 index 00000000..62fb33c0 --- /dev/null +++ b/packages/client/src/helpers/lru-cache.ts @@ -0,0 +1,149 @@ +import { safeJSONParse, safeJSONStringify } from './json'; +import { setStorage, getStorage } from './storage'; + +class Node { + public key: string; + public value: string | number; + public prev: Node | null; + public next: Node | null; + + constructor(key, value) { + this.key = key; + this.value = value; + this.prev = null; + this.next = null; + } +} + +export class LRUCache { + private capacity: number; + private usedCapacity: number; + private head: Node; + private tail: Node; + private store: Record; + + constructor(capacity) { + this.capacity = capacity || 20; + this.usedCapacity = 0; + this.store = {}; + this.head = new Node('fakeHeadNode', 'fakeHeadNode'); + this.tail = new Node('fakeTailNode', 'fakeTailNode'); + + this.head.next = this.tail; + this.tail.prev = this.head; + } + + private removeNode(node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + private addToHead(node) { + node.prev = this.head; + node.next = this.head.next; + this.head.next.prev = node; + this.head.next = node; + } + + private moveToHead(node) { + this.removeNode(node); + this.addToHead(node); + } + + private removeTail() { + const node = this.tail.prev; + this.removeNode(node); + return node; + } + + get(key) { + if (key in this.store) { + const node = this.store[key]; + this.moveToHead(node); + return node.value; + } + + return -1; + } + + put(key, value) { + if (key in this.store) { + const node = this.store[key]; + node.value = value; + this.moveToHead(node); + } else { + const node = new Node(key, value); + this.addToHead(node); + this.store[key] = node; + this.usedCapacity += 1; + + if (this.usedCapacity > this.capacity) { + const tailNode = this.removeTail(); + delete this.store[tailNode.key]; + this.usedCapacity -= 1; + } + } + } + + keys() { + const res = []; + let node = this.head; + + while (node) { + res.push(node.key); + node = node.next; + } + + return res.slice(1, -1); + } + + values() { + const res = []; + let node = this.head; + + while (node) { + res.push(node.value); + node = node.next; + } + + return res.slice(1, -1); + } + + toJSON() { + return this.store; + } +} + +const USED_STORAGE_KEYS = []; + +export const createKeysLocalStorageLRUCache = (storageKey, capacity) => { + const lruCache = new LRUCache(capacity); + + if (USED_STORAGE_KEYS.includes(storageKey)) { + throw new Error(`Storage Key ${storageKey} has been used!`); + } + + USED_STORAGE_KEYS.push(storageKey); + + return { + syncFromStorage() { + const data = getStorage(storageKey) || []; + data + .slice() + .reverse() + .forEach((key) => { + lruCache.put(key, key); + }); + }, + syncToStorage() { + setStorage(storageKey, safeJSONStringify(lruCache.keys())); + }, + put(key) { + lruCache.put(key, key); + this.syncToStorage(); + }, + get(key?: string) { + return key ? lruCache.get(key) : lruCache.keys(); + }, + }; +};