From 84783bf33e18757fd1334bb57621baa066d21621 Mon Sep 17 00:00:00 2001 From: Rene Haas Date: Mon, 18 Jul 2022 12:29:37 +0200 Subject: [PATCH] update will sync observers, updated only called if something changed --- .../src/observers/domObserver.ts | 50 +++++--- .../src/observers/trinsicObserver.ts | 53 +++++--- .../structureSetup.observers.ts | 113 ++++++++++++++---- .../setups/structureSetup/structureSetup.ts | 25 +++- .../trinsicObserver/index.browser.ts | 5 +- 5 files changed, 181 insertions(+), 65 deletions(-) diff --git a/packages/overlayscrollbars/src/observers/domObserver.ts b/packages/overlayscrollbars/src/observers/domObserver.ts index 7803ad1..0b95221 100644 --- a/packages/overlayscrollbars/src/observers/domObserver.ts +++ b/packages/overlayscrollbars/src/observers/domObserver.ts @@ -66,7 +66,10 @@ export type DOMObserverOptions = ContentObserve ? DOMContentObserverOptions : DOMTargetObserverOptions; -export type DOMObserver = [destroy: () => void, update: () => void]; +export type DOMObserver = [ + destroy: () => void, + update: () => void | false | Parameters> +]; type EventContentChangeUpdateElement = (getElements?: (selector: string) => Node[]) => void; type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement]; @@ -156,7 +159,7 @@ export const createDOMObserver = ( isContentObserver: ContentObserver, callback: DOMObserverCallback, options?: DOMObserverOptions -): DOMObserver => { +): DOMObserver => { let isConnected = false; const { _attributes, @@ -166,16 +169,17 @@ export const createDOMObserver = ( _ignoreTargetChange, _ignoreContentChange, } = (options as DOMContentObserverOptions & DOMTargetObserverOptions) || {}; + const debouncedEventContentChange = debounce( + () => { + if (isConnected) { + (callback as DOMContentObserverCallback)(true); + } + }, + { _timeout: 33, _maxDelay: 99 } + ); const [destroyEventContentChange, updateEventContentChangeElements] = createEventContentChange( target, - debounce( - () => { - if (isConnected) { - (callback as DOMContentObserverCallback)(true); - } - }, - { _timeout: 33, _maxDelay: 99 } - ), + debouncedEventContentChange, _eventContentChange ); @@ -183,7 +187,10 @@ export const createDOMObserver = ( const finalAttributes = _attributes || []; const finalStyleChangingAttributes = _styleChangingAttributes || []; const observedAttributes = finalAttributes.concat(finalStyleChangingAttributes); - const observerCallback = (mutations: MutationRecord[]) => { + const observerCallback = ( + mutations: MutationRecord[], + fromRecords?: true + ): void | Parameters> => { const ignoreTargetChange = _ignoreTargetChange || noop; const ignoreContentChange = _ignoreContentChange || noop; const targetChangedAttrs: string[] = []; @@ -244,12 +251,20 @@ export const createDOMObserver = ( } if (isContentObserver) { - contentChanged && (callback as DOMContentObserverCallback)(false); - } else if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) { - (callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged); + !fromRecords && contentChanged && (callback as DOMContentObserverCallback)(false); + return [false] as Parameters>; + } + if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) { + !fromRecords && + (callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged); + return [targetChangedAttrs, targetStyleChanged] as Parameters< + DOMObserverCallback + >; } }; - const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback); + const mutationObserver: MutationObserver = new MutationObserverConstructor!((mutations) => + observerCallback(mutations) + ); // Connect mutationObserver.observe(target, { @@ -272,7 +287,10 @@ export const createDOMObserver = ( }, () => { if (isConnected) { - observerCallback(mutationObserver.takeRecords()); + debouncedEventContentChange._flush(); + + const records = mutationObserver.takeRecords(); + return !isEmptyArray(records) && observerCallback(records, true); } }, ]; diff --git a/packages/overlayscrollbars/src/observers/trinsicObserver.ts b/packages/overlayscrollbars/src/observers/trinsicObserver.ts index 78dc62e..54ed549 100644 --- a/packages/overlayscrollbars/src/observers/trinsicObserver.ts +++ b/packages/overlayscrollbars/src/observers/trinsicObserver.ts @@ -13,7 +13,11 @@ import { import { createSizeObserver } from 'observers/sizeObserver'; import { classNameTrinsicObserver } from 'classnames'; -export type DestroyTrinsicObserver = () => void; +export type TrinsicObserverCallback = (heightIntrinsic: CacheValues) => any; +export type TrinsicObserver = [ + destroy: () => void, + update: () => void | Parameters +]; const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH): boolean => (ioEntryOrSize as WH).h === 0 || @@ -28,39 +32,45 @@ const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH */ export const createTrinsicObserver = ( target: HTMLElement, - onTrinsicChangedCallback: (heightIntrinsic: CacheValues) => any -): DestroyTrinsicObserver => { + onTrinsicChangedCallback: TrinsicObserverCallback +): TrinsicObserver => { + let intersectionObserverInstance: undefined | IntersectionObserver; const trinsicObserver = createDiv(classNameTrinsicObserver); const offListeners: (() => void)[] = []; const [updateHeightIntrinsicCache] = createCache({ _initialValue: false, }); - const triggerOnTrinsicChangedCallback = ( - updateValue?: IntersectionObserverEntry | WH - ) => { + updateValue?: IntersectionObserverEntry | WH, + fromRecords?: true + ): void | Parameters => { if (updateValue) { const heightIntrinsic = updateHeightIntrinsicCache(isHeightIntrinsic(updateValue)); const [, heightIntrinsicChanged] = heightIntrinsic; if (heightIntrinsicChanged) { - onTrinsicChangedCallback(heightIntrinsic); + !fromRecords && onTrinsicChangedCallback(heightIntrinsic); + return [heightIntrinsic]; } } }; + const intersectionObserverCallback = ( + entries: IntersectionObserverEntry[], + fromRecords?: true + ) => { + if (entries && entries.length > 0) { + return triggerOnTrinsicChangedCallback(entries.pop(), fromRecords); + } + }; if (IntersectionObserverConstructor) { - const intersectionObserverInstance: IntersectionObserver = new IntersectionObserverConstructor( - (entries: IntersectionObserverEntry[]) => { - if (entries && entries.length > 0) { - triggerOnTrinsicChangedCallback(entries.pop()); - } - }, + intersectionObserverInstance = new IntersectionObserverConstructor( + (entries) => intersectionObserverCallback(entries), { root: target } ); intersectionObserverInstance.observe(trinsicObserver); push(offListeners, () => { - intersectionObserverInstance.disconnect(); + intersectionObserverInstance!.disconnect(); }); } else { const onSizeChanged = () => { @@ -73,8 +83,15 @@ export const createTrinsicObserver = ( prependChildren(target, trinsicObserver); - return () => { - runEachAndClear(offListeners); - removeElements(trinsicObserver); - }; + return [ + () => { + runEachAndClear(offListeners); + removeElements(trinsicObserver); + }, + () => { + if (intersectionObserverInstance) { + return intersectionObserverCallback(intersectionObserverInstance.takeRecords(), true); + } + }, + ]; }; diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.observers.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.observers.ts index 4a6ad6b..4b380cf 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.observers.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.observers.ts @@ -18,6 +18,8 @@ import { isFunction, ResizeObserverConstructor, closest, + assignDeep, + push, } from 'support'; import { getEnvironment } from 'environment'; import { @@ -41,8 +43,9 @@ import type { export type StructureSetupObserversUpdate = (checkOption: SetupUpdateCheckOption) => void; export type StructureSetupObservers = [ - updateObserverOptions: StructureSetupObserversUpdate, - destroy: () => void + destroy: () => void, + updateObservers: () => Partial, + updateObserversOptions: StructureSetupObserversUpdate ]; type ExcludeFromTuple = T extends [infer F, ...infer R] @@ -69,7 +72,7 @@ export const createStructureSetupObservers = ( ): StructureSetupObservers => { let debounceTimeout: number | false | undefined; let debounceMaxDelay: number | false | undefined; - let contentMutationObserver: DOMObserver | undefined; + let contentMutationObserver: DOMObserver | undefined; const [, setState] = state; const { _host, @@ -134,10 +137,14 @@ export const createStructureSetupObservers = ( } }); }; - const onTrinsicChanged = (heightIntrinsicCache: CacheValues) => { + const onTrinsicChanged = (heightIntrinsicCache: CacheValues, fromRecords?: true) => { const [heightIntrinsic, heightIntrinsicChanged] = heightIntrinsicCache; + const updateHints: Partial = { + _heightIntrinsicChanged: heightIntrinsicChanged, + }; setState({ _heightIntrinsic: heightIntrinsic }); - structureSetupUpdate({ _heightIntrinsicChanged: heightIntrinsicChanged }); + !fromRecords && structureSetupUpdate(updateHints); + return updateHints; }; const onSizeChanged = ({ _sizeChanged, @@ -158,30 +165,36 @@ export const createStructureSetupObservers = ( updateFn({ _sizeChanged, _directionChanged: directionChanged }); }; - const onContentMutation = (contentChangedTroughEvent: boolean) => { + const onContentMutation = (contentChangedTroughEvent: boolean, fromRecords?: true) => { const [, contentSizeChanged] = updateContentSizeCache(); + const updateHints: Partial = { + _contentMutation: contentSizeChanged, + }; // if contentChangedTroughEvent is true its already debounced const updateFn = contentChangedTroughEvent ? structureSetupUpdate : structureSetupUpdateWithDebouncedAdaptiveUpdateHints; if (contentSizeChanged) { - updateFn({ - _contentMutation: true, - }); + !fromRecords && updateFn(updateHints); } + return updateHints; }; - const onHostMutation = (targetChangedAttrs: string[], targetStyleChanged: boolean) => { + const onHostMutation = ( + targetChangedAttrs: string[], + targetStyleChanged: boolean, + fromRecords?: true + ) => { + const updateHints: Partial = { _hostMutation: targetStyleChanged }; if (targetStyleChanged) { - structureSetupUpdateWithDebouncedAdaptiveUpdateHints({ - _hostMutation: true, - }); + !fromRecords && structureSetupUpdateWithDebouncedAdaptiveUpdateHints(updateHints); } else if (!_viewportIsTarget) { updateViewportAttrsFromHost(targetChangedAttrs); } + return updateHints; }; - const destroyTrinsicObserver = + const trinsicObserver = (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged); const destroySizeObserver = !_viewportIsTarget && @@ -189,10 +202,15 @@ export const createStructureSetupObservers = ( _appear: true, _direction: !_nativeScrollbarStyling, }); - const [destroyHostMutationObserver] = createDOMObserver(_host, false, onHostMutation, { - _styleChangingAttributes: baseStyleChangingAttrs, - _attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget), - }); + const [destroyHostMutationObserver, updateHostMutationObserver] = createDOMObserver( + _host, + false, + onHostMutation, + { + _styleChangingAttributes: baseStyleChangingAttrs, + _attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget), + } + ); const viewportIsTargetResizeObserver = _viewportIsTarget && @@ -202,6 +220,58 @@ export const createStructureSetupObservers = ( updateViewportAttrsFromHost(); return [ + () => { + contentMutationObserver && contentMutationObserver[0](); // destroy + trinsicObserver && trinsicObserver[0](); // destroy + destroySizeObserver && destroySizeObserver(); + viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect(); + destroyHostMutationObserver(); + }, + () => { + const updateHints: Partial = {}; + const hostUpdateResult = updateHostMutationObserver(); + const contentUpdateResult = contentMutationObserver && contentMutationObserver[1](); // update + const trinsicUpdateResult = trinsicObserver && trinsicObserver[1](); // update + + if (hostUpdateResult) { + assignDeep( + updateHints, + onHostMutation.apply( + 0, + push(hostUpdateResult, true) as [ + ...updateResult: typeof hostUpdateResult, + fromRecords: true + ] + ) + ); + } + if (contentUpdateResult) { + assignDeep( + updateHints, + onContentMutation.apply( + 0, + push(contentUpdateResult, true) as [ + ...updateResult: typeof contentUpdateResult, + fromRecords: true + ] + ) + ); + } + if (trinsicUpdateResult) { + assignDeep( + updateHints, + onTrinsicChanged.apply( + 0, + push(trinsicUpdateResult as any[], true) as [ + ...updateResult: typeof trinsicUpdateResult, + fromRecords: true + ] + ) + ); + } + + return updateHints; + }, (checkOption) => { const [ignoreMutation] = checkOption('updating.ignoreMutation'); const [attributes, attributesChanged] = checkOption('updating.attributes'); @@ -261,12 +331,5 @@ export const createStructureSetupObservers = ( } } }, - () => { - contentMutationObserver && contentMutationObserver[0](); // destroy - destroyTrinsicObserver && destroyTrinsicObserver(); - destroySizeObserver && destroySizeObserver(); - viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect(); - destroyHostMutationObserver(); - }, ]; }; diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts index 11c5577..12bee77 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts @@ -1,4 +1,4 @@ -import { createEventListenerHub } from 'support'; +import { createEventListenerHub, isEmptyObject, keys } from 'support'; import { createState, createOptionCheck } from 'setups/setups'; import { createStructureSetupElements } from 'setups/structureSetup/structureSetup.elements'; import { createStructureSetupUpdate } from 'setups/structureSetup/structureSetup.update'; @@ -79,11 +79,22 @@ export const createStructureSetup = ( const [getState] = state; const [elements, appendElements, destroyElements] = createStructureSetupElements(target); const updateStructure = createStructureSetupUpdate(elements, state); - const [updateObservers, destroyObservers] = createStructureSetupObservers( + const triggerUpdateEvent: (...args: StructureSetupEventMap['u']) => void = ( + updateHints, + changedOptions, + force + ) => { + const truthyUpdateHints = keys(updateHints).some((key) => updateHints[key]); + + if (truthyUpdateHints || !isEmptyObject(changedOptions) || force) { + triggerEvent('u', [updateHints, changedOptions, force]); + } + }; + const [destroyObservers, updateObservers, updateObserversOptions] = createStructureSetupObservers( elements, state, (updateHints) => { - triggerEvent('u', [updateStructure(checkOptionsFallback, updateHints), {}, false]); + triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false); } ); @@ -98,8 +109,12 @@ export const createStructureSetup = ( return [ (changedOptions, force?) => { const checkOption = createOptionCheck(options, changedOptions, force); - updateObservers(checkOption); - triggerEvent('u', [updateStructure(checkOption, {}, force), changedOptions, !!force]); + updateObserversOptions(checkOption); + triggerUpdateEvent( + updateStructure(checkOption, updateObservers(), force), + changedOptions, + !!force + ); }, structureSetupState, () => { diff --git a/packages/overlayscrollbars/tests/playwright/observers/trinsicObserver/index.browser.ts b/packages/overlayscrollbars/tests/playwright/observers/trinsicObserver/index.browser.ts index 82e726a..bea2880 100644 --- a/packages/overlayscrollbars/tests/playwright/observers/trinsicObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/playwright/observers/trinsicObserver/index.browser.ts @@ -29,7 +29,7 @@ const startBtn: HTMLButtonElement | null = document.querySelector('#start'); const changesSlot: HTMLButtonElement | null = document.querySelector('#changes'); const preInitChildren = targetElm?.children.length; -const destroyTrinsicObserver = createTrinsicObserver( +const [destroyTrinsicObserver, updateTrinsicObserver] = createTrinsicObserver( targetElm as HTMLElement, (heightIntrinsicCache) => { const [currentHeightIntrinsic, currentHeightIntrinsicChanged] = heightIntrinsicCache; @@ -158,6 +158,9 @@ const start = async () => { }); await changeWhileHidden(); + updateTrinsicObserver(); + destroyTrinsicObserver(); + updateTrinsicObserver(); destroyTrinsicObserver(); should.equal( targetElm?.children.length,