update will sync observers, updated only called if something changed

This commit is contained in:
Rene Haas
2022-07-18 12:29:37 +02:00
parent cd9c01a588
commit 84783bf33e
5 changed files with 181 additions and 65 deletions
@@ -66,7 +66,10 @@ export type DOMObserverOptions<ContentObserver extends boolean> = ContentObserve
? DOMContentObserverOptions
: DOMTargetObserverOptions;
export type DOMObserver = [destroy: () => void, update: () => void];
export type DOMObserver<ContentObserver extends boolean> = [
destroy: () => void,
update: () => void | false | Parameters<DOMObserverCallback<ContentObserver>>
];
type EventContentChangeUpdateElement = (getElements?: (selector: string) => Node[]) => void;
type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement];
@@ -156,7 +159,7 @@ export const createDOMObserver = <ContentObserver extends boolean>(
isContentObserver: ContentObserver,
callback: DOMObserverCallback<ContentObserver>,
options?: DOMObserverOptions<ContentObserver>
): DOMObserver => {
): DOMObserver<ContentObserver> => {
let isConnected = false;
const {
_attributes,
@@ -166,16 +169,17 @@ export const createDOMObserver = <ContentObserver extends boolean>(
_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 = <ContentObserver extends boolean>(
const finalAttributes = _attributes || [];
const finalStyleChangingAttributes = _styleChangingAttributes || [];
const observedAttributes = finalAttributes.concat(finalStyleChangingAttributes);
const observerCallback = (mutations: MutationRecord[]) => {
const observerCallback = (
mutations: MutationRecord[],
fromRecords?: true
): void | Parameters<DOMObserverCallback<ContentObserver>> => {
const ignoreTargetChange = _ignoreTargetChange || noop;
const ignoreContentChange = _ignoreContentChange || noop;
const targetChangedAttrs: string[] = [];
@@ -244,12 +251,20 @@ export const createDOMObserver = <ContentObserver extends boolean>(
}
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<DOMObserverCallback<ContentObserver>>;
}
if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) {
!fromRecords &&
(callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged);
return [targetChangedAttrs, targetStyleChanged] as Parameters<
DOMObserverCallback<ContentObserver>
>;
}
};
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 = <ContentObserver extends boolean>(
},
() => {
if (isConnected) {
observerCallback(mutationObserver.takeRecords());
debouncedEventContentChange._flush();
const records = mutationObserver.takeRecords();
return !isEmptyArray(records) && observerCallback(records, true);
}
},
];
@@ -13,7 +13,11 @@ import {
import { createSizeObserver } from 'observers/sizeObserver';
import { classNameTrinsicObserver } from 'classnames';
export type DestroyTrinsicObserver = () => void;
export type TrinsicObserverCallback = (heightIntrinsic: CacheValues<boolean>) => any;
export type TrinsicObserver = [
destroy: () => void,
update: () => void | Parameters<TrinsicObserverCallback>
];
const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH<number>): boolean =>
(ioEntryOrSize as WH<number>).h === 0 ||
@@ -28,39 +32,45 @@ const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH<number>
*/
export const createTrinsicObserver = (
target: HTMLElement,
onTrinsicChangedCallback: (heightIntrinsic: CacheValues<boolean>) => 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<number>
) => {
updateValue?: IntersectionObserverEntry | WH<number>,
fromRecords?: true
): void | Parameters<TrinsicObserverCallback> => {
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);
}
},
];
};
@@ -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<StructureSetupUpdateHints>,
updateObserversOptions: StructureSetupObserversUpdate
];
type ExcludeFromTuple<T extends readonly any[], E> = 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<true> | undefined;
const [, setState] = state;
const {
_host,
@@ -134,10 +137,14 @@ export const createStructureSetupObservers = (
}
});
};
const onTrinsicChanged = (heightIntrinsicCache: CacheValues<boolean>) => {
const onTrinsicChanged = (heightIntrinsicCache: CacheValues<boolean>, fromRecords?: true) => {
const [heightIntrinsic, heightIntrinsicChanged] = heightIntrinsicCache;
const updateHints: Partial<StructureSetupUpdateHints> = {
_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<StructureSetupUpdateHints> = {
_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<StructureSetupUpdateHints> = { _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<StructureSetupUpdateHints> = {};
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<string[] | null>('updating.ignoreMutation');
const [attributes, attributesChanged] = checkOption<string[] | null>('updating.attributes');
@@ -261,12 +331,5 @@ export const createStructureSetupObservers = (
}
}
},
() => {
contentMutationObserver && contentMutationObserver[0](); // destroy
destroyTrinsicObserver && destroyTrinsicObserver();
destroySizeObserver && destroySizeObserver();
viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect();
destroyHostMutationObserver();
},
];
};
@@ -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,
() => {
@@ -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,