From 2caea68605deea51d61eec4a018b515bb0bd210b Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Tue, 16 Jul 2024 00:55:04 +0800 Subject: [PATCH] [gem] Improve light dom apply style performance --- packages/gem-examples/src/theme/index.ts | 35 ++++++++------- packages/gem/src/helper/theme.ts | 6 +-- packages/gem/src/lib/decorators.ts | 2 +- packages/gem/src/lib/element.ts | 54 +++++++++++++++++------- packages/gem/src/lib/utils.ts | 11 ----- 5 files changed, 63 insertions(+), 45 deletions(-) diff --git a/packages/gem-examples/src/theme/index.ts b/packages/gem-examples/src/theme/index.ts index 32f0e382..9f952763 100644 --- a/packages/gem-examples/src/theme/index.ts +++ b/packages/gem-examples/src/theme/index.ts @@ -1,4 +1,4 @@ -import { GemElement, html, render, customElement, connectStore } from '@mantou/gem'; +import { GemElement, html, render, customElement, connectStore, createCSSSheet, css, adoptedStyle } from '@mantou/gem'; import { createTheme, getThemeStore, updateTheme } from '@mantou/gem/helper/theme'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; @@ -25,24 +25,29 @@ document.onclick = () => { }); }; +const style = createCSSSheet(css` + div { + color: rgba(${theme.color}, 0.5); + border: 2px solid ${theme.primaryColor}; + } +`); + +const style1 = createCSSSheet( + css` + div { + border: 2px solid ${printTheme.primaryColor}; + } + `, + mediaQuery.PRINT, +); + @customElement('app-root') @connectStore(themeStore) +@adoptedStyle(style1) +@adoptedStyle(style) export class App extends GemElement { render() { - return html` - -
color: ${themeStore.primaryColor}
- `; + return html`
color: ${themeStore.primaryColor}
`; } } diff --git a/packages/gem/src/helper/theme.ts b/packages/gem/src/helper/theme.ts index 1fcd5ca1..2456d69f 100644 --- a/packages/gem/src/helper/theme.ts +++ b/packages/gem/src/helper/theme.ts @@ -32,7 +32,7 @@ const setThemeFnMap = new WeakMap(); */ export function createTheme>(themeObj: T) { const salt = randomStr(); - const style = document.createElement('style'); + const style = new CSSStyleSheet(); const store = createStore(themeObj); const theme: Record = {}; const props: Record = {}; @@ -48,10 +48,10 @@ export function createTheme>(themeObj: T) { setTheme(); const getStyle = () => `:root, :host {${Object.keys(store).reduce((prev, key) => prev + `${props[key]}:${store[key]};`, '')}}`; - const replace = () => (style.textContent = getStyle()); + const replace = () => style.replaceSync(getStyle()); connect(store, replace); replace(); - (document.head || document.documentElement).append(style); + document.adoptedStyleSheets.push(style); return theme as SomeType; } diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 84f38ca3..1d6ae7c7 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -466,7 +466,7 @@ function defineEmitter( } /** - * 分配一个构造的样式表,如果元素是 lightDOM,则将样式表挂载到 RootNode 上 + * 分配一个构造的样式表,前面的优先级越高(因为装饰器是越近越早执行) * * For example * ```ts diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 926a4d21..cf4e4475 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -1,16 +1,7 @@ import { html, render, TemplateResult } from 'lit-html'; import { connect, Store } from './store'; -import { - LinkedList, - addMicrotask, - Sheet, - SheetToken, - isArrayChange, - GemError, - removeItems, - addListener, -} from './utils'; +import { LinkedList, addMicrotask, Sheet, SheetToken, isArrayChange, GemError, addListener } from './utils'; export { html, svg, render, directive, TemplateResult, SVGTemplateResult } from 'lit-html'; @@ -47,6 +38,43 @@ const tick = (timeStamp = performance.now()) => { }; noBlockingTaskList.addEventListener('start', () => addMicrotask(tick)); +const rootStyleSheetInfo = new WeakMap>(); +const rootUpdateFnMap = new WeakMap, () => void>(); + +const updateStyleSheets = (map: Map, sheets: CSSStyleSheet[], value: number) => { + let needUpdate = false; + sheets.forEach((e) => { + const count = map.get(e) || 0; + needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1); + // count 为 0 时不立即删除,以便更新时识别是否外部样式 + // 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC? + map.set(e, count + value); + }); + if (needUpdate) { + // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新 + addMicrotask(rootUpdateFnMap.get(map)!); + } +}; + +export function appleCSSStyleSheet(ele: HTMLElement, sheets: CSSStyleSheet[]) { + const root = ele.getRootNode() as ShadowRoot | Document; + if (!rootStyleSheetInfo.has(root)) { + const map = new Map(); + rootStyleSheetInfo.set(root, map); + rootUpdateFnMap.set(map, () => { + // 先找到外部样式表 + const newSheets = root.adoptedStyleSheets.filter((e) => !map.has(e)); + map.forEach((count, sheet) => count && newSheets.push(sheet)); + root.adoptedStyleSheets = newSheets; + }); + } + + const map = rootStyleSheetInfo.get(root)!; + + updateStyleSheets(map, sheets, 1); + return () => updateStyleSheets(map, sheets, -1); +} + type GetDepFun = () => T; type EffectCallback = (depValues: T, oldDepValues?: T) => any; type EffectItem = { @@ -147,11 +175,7 @@ export abstract class GemElement> extends HTMLEl () => { // 阻止其他元素应用样式到当前元素 this.dataset.styleScope = ''; - const root = this.getRootNode() as ShadowRoot | Document; - root.adoptedStyleSheets.push(...sheets); - return () => { - root.adoptedStyleSheets = removeItems(root.adoptedStyleSheets, sheets); - }; + return appleCSSStyleSheet(this, sheets); }, () => [], ); diff --git a/packages/gem/src/lib/utils.ts b/packages/gem/src/lib/utils.ts index 8f3be230..af17852d 100644 --- a/packages/gem/src/lib/utils.ts +++ b/packages/gem/src/lib/utils.ts @@ -348,17 +348,6 @@ export function isArrayChange(newValues: any[], oldValues: any[]) { return false; } -export function removeItems(target: any[], items: any[]) { - const set = new Set(items); - return target.filter((e) => { - if (set.has(e)) { - set.delete(e); - return false; - } - return true; - }); -} - export function objectMapToString( object: Record, separate: string,