mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 06:55:26 +00:00
Revert "perf(vector-node): batch resize sync and cut redundant rerenders"
This reverts commit e344c2272e.
This commit is contained in:
@@ -193,18 +193,18 @@
|
|||||||
- [x] 用户素材删除与持久化通过(删除后刷新不复活)。
|
- [x] 用户素材删除与持久化通过(删除后刷新不复活)。
|
||||||
- [ ] 缺失资产降级策略通过(不阻断导出/渲染)。
|
- [ ] 缺失资产降级策略通过(不阻断导出/渲染)。
|
||||||
- [x] Dynamic Group 分组基础行为通过(分组信息写入 `meta.groupId`,复制分组会携带组内节点)。
|
- [x] Dynamic Group 分组基础行为通过(分组信息写入 `meta.groupId`,复制分组会携带组内节点)。
|
||||||
- [x] 分组规则静态检查通过(冲突与供火提示正确且可实时更新)。
|
- [ ] 分组规则静态检查通过(冲突与供火提示正确且可实时更新)。
|
||||||
- [x] 规则管理通过(规则列表表格化、弹窗编辑、导入导出可用)。
|
- [ ] 规则管理通过(规则列表表格化、弹窗编辑、导入导出可用)。
|
||||||
- [x] 矢量节点快速缩放性能回归通过(无明显卡顿/卡死)。
|
- [ ] 矢量节点快速缩放性能回归通过(无明显卡顿/卡死)。
|
||||||
- [ ] 导出到 wiki 数据兼容通过(wiki 侧可 normalize 与预览)。
|
- [ ] 导出到 wiki 数据兼容通过(wiki 侧可 normalize 与预览)。
|
||||||
- [ ] 跨项目素材互通通过(同 origin 可复用素材,跨 origin 不互通)。
|
- [ ] 跨项目素材互通通过(同 origin 可复用素材,跨 origin 不互通)。
|
||||||
- [ ] 跨项目规则互通方案确认(共享配置源定义、两侧读取一致)。
|
- [ ] 跨项目规则互通方案确认(共享配置源定义、两侧读取一致)。
|
||||||
- [x] 导出图片时隐藏 Dynamic Group 通过(导出前隐藏,导出后恢复)。
|
- [x] 导出图片时隐藏 Dynamic Group 通过(导出前隐藏,导出后恢复)。
|
||||||
|
|
||||||
当前状态(2026-02-27):
|
当前状态(2026-02-27):
|
||||||
- 已通过:8 项(基础启动与构建、用户素材上传与使用、用户素材删除与持久化、Dynamic Group 分组基础行为、分组规则静态检查、规则管理、矢量节点快速缩放性能回归、导出图片时隐藏 Dynamic Group)。
|
- 已通过:5 项(基础启动与构建、用户素材上传与使用、用户素材删除与持久化、Dynamic Group 分组基础行为、导出图片时隐藏 Dynamic Group)。
|
||||||
- 部分通过:1 项(跨项目规则互通方案确认)。
|
- 部分通过:1 项(跨项目规则互通方案确认)。
|
||||||
- 未通过/待验证:4 项(其余项待完整手测或跨仓联调)。
|
- 未通过/待验证:7 项(其余项待完整手测或跨仓联调)。
|
||||||
|
|
||||||
逐项状态:
|
逐项状态:
|
||||||
- 基础启动与构建:已通过
|
- 基础启动与构建:已通过
|
||||||
@@ -213,9 +213,9 @@
|
|||||||
- 用户素材删除与持久化:已通过
|
- 用户素材删除与持久化:已通过
|
||||||
- 缺失资产降级策略:未通过(待手测)
|
- 缺失资产降级策略:未通过(待手测)
|
||||||
- Dynamic Group 分组基础行为:已通过
|
- Dynamic Group 分组基础行为:已通过
|
||||||
- 分组规则静态检查:已通过
|
- 分组规则静态检查:未通过(待手测)
|
||||||
- 规则管理(表格化/导入导出):已通过
|
- 规则管理(表格化/导入导出):未通过(待手测)
|
||||||
- 矢量节点快速缩放性能回归:已通过
|
- 矢量节点快速缩放性能回归:未通过(待手测)
|
||||||
- 导出到 wiki 数据兼容:未通过(待跨仓联测)
|
- 导出到 wiki 数据兼容:未通过(待跨仓联测)
|
||||||
- 跨项目素材互通:未通过(待同 origin 联测)
|
- 跨项目素材互通:未通过(待同 origin 联测)
|
||||||
- 跨项目规则互通方案确认:部分通过(yys-editor 已落地,wiki 待读取同源配置)
|
- 跨项目规则互通方案确认:部分通过(yys-editor 已落地,wiki 待读取同源配置)
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
DEFAULT_VECTOR_CONFIG,
|
|
||||||
buildNextVectorConfig,
|
|
||||||
createRafLatestScheduler,
|
|
||||||
type VectorConfig
|
|
||||||
} from '@/components/flow/nodes/common/vectorNodeSync';
|
|
||||||
|
|
||||||
function createFakeRaf() {
|
|
||||||
let id = 0;
|
|
||||||
const callbacks = new Map<number, FrameRequestCallback>();
|
|
||||||
|
|
||||||
const requestFrame = vi.fn((cb: FrameRequestCallback) => {
|
|
||||||
id += 1;
|
|
||||||
callbacks.set(id, cb);
|
|
||||||
return id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancelFrame = vi.fn((handle: number) => {
|
|
||||||
callbacks.delete(handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
const runFrame = () => {
|
|
||||||
const pending = Array.from(callbacks.values());
|
|
||||||
callbacks.clear();
|
|
||||||
pending.forEach((cb) => cb(Date.now()));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestFrame,
|
|
||||||
cancelFrame,
|
|
||||||
runFrame
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('vectorNodeSync', () => {
|
|
||||||
it('只在 vector 配置变化时生成下一次提交配置', () => {
|
|
||||||
const current: VectorConfig = { ...DEFAULT_VECTOR_CONFIG };
|
|
||||||
|
|
||||||
expect(buildNextVectorConfig(current, { ...current })).toBeNull();
|
|
||||||
|
|
||||||
const next = buildNextVectorConfig(current, {
|
|
||||||
fill: '#000000',
|
|
||||||
tileWidth: 64
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(next).toMatchObject({
|
|
||||||
fill: '#000000',
|
|
||||||
tileWidth: 64
|
|
||||||
});
|
|
||||||
expect(next?.tileHeight).toBe(current.tileHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('同一帧连续缩放事件只提交最后一次更新,避免重复抖动', () => {
|
|
||||||
const fakeRaf = createFakeRaf();
|
|
||||||
const commits: VectorConfig[] = [];
|
|
||||||
const scheduler = createRafLatestScheduler<VectorConfig>(
|
|
||||||
(payload) => {
|
|
||||||
commits.push(payload);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
requestFrame: fakeRaf.requestFrame,
|
|
||||||
cancelFrame: fakeRaf.cancelFrame
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
scheduler.enqueue({ ...DEFAULT_VECTOR_CONFIG, strokeWidth: 1 });
|
|
||||||
scheduler.enqueue({ ...DEFAULT_VECTOR_CONFIG, strokeWidth: 2 });
|
|
||||||
scheduler.enqueue({ ...DEFAULT_VECTOR_CONFIG, strokeWidth: 3 });
|
|
||||||
|
|
||||||
expect(fakeRaf.requestFrame).toHaveBeenCalledTimes(1);
|
|
||||||
expect(commits).toHaveLength(0);
|
|
||||||
|
|
||||||
fakeRaf.runFrame();
|
|
||||||
|
|
||||||
expect(commits).toHaveLength(1);
|
|
||||||
expect(commits[0].strokeWidth).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('连续缩放仅变化尺寸时不会触发矢量配置重复提交', () => {
|
|
||||||
const fakeRaf = createFakeRaf();
|
|
||||||
const commits: VectorConfig[] = [];
|
|
||||||
const scheduler = createRafLatestScheduler<VectorConfig>(
|
|
||||||
(payload) => {
|
|
||||||
commits.push(payload);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
requestFrame: fakeRaf.requestFrame,
|
|
||||||
cancelFrame: fakeRaf.cancelFrame
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const current = { ...DEFAULT_VECTOR_CONFIG };
|
|
||||||
for (let i = 0; i < 40; i += 1) {
|
|
||||||
const next = buildNextVectorConfig(current, { ...current });
|
|
||||||
if (next) {
|
|
||||||
scheduler.enqueue(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(fakeRaf.requestFrame).not.toHaveBeenCalled();
|
|
||||||
expect(commits).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,95 +1,120 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||||
import {
|
|
||||||
DEFAULT_VECTOR_CONFIG,
|
|
||||||
buildNextVectorConfig,
|
|
||||||
createRafLatestScheduler,
|
|
||||||
type VectorConfig
|
|
||||||
} from './vectorNodeSync';
|
|
||||||
|
|
||||||
const vectorConfig = ref<VectorConfig>({ ...DEFAULT_VECTOR_CONFIG });
|
const vectorConfig = ref({
|
||||||
const patternId = `vector-pattern-${Math.random().toString(36).slice(2, 11)}`;
|
kind: 'rect',
|
||||||
|
svgContent: '',
|
||||||
const syncVectorConfig = createRafLatestScheduler<VectorConfig>((nextConfig) => {
|
path: '',
|
||||||
vectorConfig.value = nextConfig;
|
tileWidth: 50,
|
||||||
|
tileHeight: 50,
|
||||||
|
fill: '#409EFF',
|
||||||
|
stroke: '#303133',
|
||||||
|
strokeWidth: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeSize = ref({ width: 200, height: 200 });
|
||||||
|
let syncRafId: number | null = null;
|
||||||
|
let pendingVectorConfig: Record<string, any> | null = null;
|
||||||
|
let pendingNodeSize: { width: number; height: number } | null = null;
|
||||||
|
|
||||||
|
const flushPendingSync = () => {
|
||||||
|
if (pendingVectorConfig) {
|
||||||
|
Object.assign(vectorConfig.value, pendingVectorConfig);
|
||||||
|
pendingVectorConfig = null;
|
||||||
|
}
|
||||||
|
if (pendingNodeSize) {
|
||||||
|
nodeSize.value = pendingNodeSize;
|
||||||
|
pendingNodeSize = null;
|
||||||
|
}
|
||||||
|
syncRafId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSync = () => {
|
||||||
|
if (typeof requestAnimationFrame === 'undefined') {
|
||||||
|
flushPendingSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (syncRafId !== null) {
|
||||||
|
cancelAnimationFrame(syncRafId);
|
||||||
|
}
|
||||||
|
syncRafId = requestAnimationFrame(flushPendingSync);
|
||||||
|
};
|
||||||
|
|
||||||
const { containerStyle } = useNodeAppearance({
|
const { containerStyle } = useNodeAppearance({
|
||||||
onPropsChange(props) {
|
onPropsChange(props, node) {
|
||||||
const nextConfig = buildNextVectorConfig(vectorConfig.value, props?.vector);
|
if (props.vector) {
|
||||||
if (nextConfig) {
|
pendingVectorConfig = { ...props.vector };
|
||||||
syncVectorConfig.enqueue(nextConfig);
|
|
||||||
}
|
}
|
||||||
|
if (node) {
|
||||||
|
pendingNodeSize = {
|
||||||
|
width: node.width,
|
||||||
|
height: node.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 使用 requestAnimationFrame 防抖,减少快速缩放时的重复重绘
|
||||||
|
scheduleSync();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
syncVectorConfig.cancel();
|
if (syncRafId !== null && typeof cancelAnimationFrame !== 'undefined') {
|
||||||
|
cancelAnimationFrame(syncRafId);
|
||||||
|
syncRafId = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ellipseCx = computed(() => vectorConfig.value.tileWidth / 2);
|
const patternId = `vector-pattern-${Math.random().toString(36).slice(2, 11)}`;
|
||||||
const ellipseCy = computed(() => vectorConfig.value.tileHeight / 2);
|
|
||||||
const ellipseRx = computed(() => Math.max(0, vectorConfig.value.tileWidth / 2 - vectorConfig.value.strokeWidth));
|
// 生成 SVG 内容
|
||||||
const ellipseRy = computed(() => Math.max(0, vectorConfig.value.tileHeight / 2 - vectorConfig.value.strokeWidth));
|
const svgContent = computed(() => {
|
||||||
const polygonPoints = computed(
|
const { kind, path, tileWidth, tileHeight, fill, stroke, strokeWidth } = vectorConfig.value;
|
||||||
() => `0,${vectorConfig.value.tileHeight} ${vectorConfig.value.tileWidth / 2},0 ${vectorConfig.value.tileWidth},${vectorConfig.value.tileHeight}`
|
|
||||||
);
|
let shapeElement = '';
|
||||||
const safePath = computed(() => vectorConfig.value.path || 'M 0 0');
|
switch (kind) {
|
||||||
const patternFill = computed(() => `url(#${patternId})`);
|
case 'rect':
|
||||||
|
shapeElement = `<rect x="0" y="0" width="${tileWidth}" height="${tileHeight}"
|
||||||
|
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||||
|
break;
|
||||||
|
case 'ellipse':
|
||||||
|
shapeElement = `<ellipse cx="${tileWidth/2}" cy="${tileHeight/2}"
|
||||||
|
rx="${tileWidth/2 - strokeWidth}" ry="${tileHeight/2 - strokeWidth}"
|
||||||
|
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||||
|
break;
|
||||||
|
case 'path':
|
||||||
|
shapeElement = `<path d="${path || 'M 0 0'}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||||
|
break;
|
||||||
|
case 'svg':
|
||||||
|
shapeElement = vectorConfig.value.svgContent || '';
|
||||||
|
break;
|
||||||
|
case 'polygon':
|
||||||
|
// 默认三角形
|
||||||
|
const points = `0,${tileHeight} ${tileWidth/2},0 ${tileWidth},${tileHeight}`;
|
||||||
|
shapeElement = `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<svg width="${nodeSize.value.width}" height="${nodeSize.value.height}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="${patternId}"
|
||||||
|
x="0" y="0"
|
||||||
|
width="${tileWidth}"
|
||||||
|
height="${tileHeight}"
|
||||||
|
patternUnits="userSpaceOnUse">
|
||||||
|
${shapeElement}
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#${patternId})" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="vector-node" :style="containerStyle">
|
<div class="vector-node" :style="containerStyle">
|
||||||
<svg class="vector-content" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
<div class="vector-content" v-html="svgContent"></div>
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
:id="patternId"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
:width="vectorConfig.tileWidth"
|
|
||||||
:height="vectorConfig.tileHeight"
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
v-if="vectorConfig.kind === 'rect'"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
:width="vectorConfig.tileWidth"
|
|
||||||
:height="vectorConfig.tileHeight"
|
|
||||||
:fill="vectorConfig.fill"
|
|
||||||
:stroke="vectorConfig.stroke"
|
|
||||||
:stroke-width="vectorConfig.strokeWidth"
|
|
||||||
/>
|
|
||||||
<ellipse
|
|
||||||
v-else-if="vectorConfig.kind === 'ellipse'"
|
|
||||||
:cx="ellipseCx"
|
|
||||||
:cy="ellipseCy"
|
|
||||||
:rx="ellipseRx"
|
|
||||||
:ry="ellipseRy"
|
|
||||||
:fill="vectorConfig.fill"
|
|
||||||
:stroke="vectorConfig.stroke"
|
|
||||||
:stroke-width="vectorConfig.strokeWidth"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
v-else-if="vectorConfig.kind === 'path'"
|
|
||||||
:d="safePath"
|
|
||||||
:fill="vectorConfig.fill"
|
|
||||||
:stroke="vectorConfig.stroke"
|
|
||||||
:stroke-width="vectorConfig.strokeWidth"
|
|
||||||
/>
|
|
||||||
<g v-else-if="vectorConfig.kind === 'svg'" v-html="vectorConfig.svgContent || ''"></g>
|
|
||||||
<polygon
|
|
||||||
v-else
|
|
||||||
:points="polygonPoints"
|
|
||||||
:fill="vectorConfig.fill"
|
|
||||||
:stroke="vectorConfig.stroke"
|
|
||||||
:stroke-width="vectorConfig.strokeWidth"
|
|
||||||
/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" :fill="patternFill" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -103,6 +128,11 @@ const patternFill = computed(() => `url(#${patternId})`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vector-content {
|
.vector-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vector-content :deep(svg) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -33,25 +33,9 @@ class VectorNodeModel extends HtmlNodeModel {
|
|||||||
resize(deltaX: number, deltaY: number) {
|
resize(deltaX: number, deltaY: number) {
|
||||||
const result = super.resize?.(deltaX, deltaY);
|
const result = super.resize?.(deltaX, deltaY);
|
||||||
|
|
||||||
const nextWidth = this.width;
|
// 持久化宽高到 properties
|
||||||
const nextHeight = this.height;
|
this.setProperty('width', this.width);
|
||||||
|
this.setProperty('height', this.height);
|
||||||
// 宽高无变化时跳过,避免高频缩放中的无效属性变更事件。
|
|
||||||
if (this.properties?.width === nextWidth && this.properties?.height === nextHeight) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 持久化宽高到 properties(单次提交,减少事件抖动)。
|
|
||||||
const setProperties = (this as any).setProperties as ((props: Record<string, any>) => void) | undefined;
|
|
||||||
if (setProperties) {
|
|
||||||
setProperties.call(this, {
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setProperty('width', nextWidth);
|
|
||||||
this.setProperty('height', nextHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
export interface VectorConfig {
|
|
||||||
kind: string;
|
|
||||||
svgContent: string;
|
|
||||||
path: string;
|
|
||||||
tileWidth: number;
|
|
||||||
tileHeight: number;
|
|
||||||
fill: string;
|
|
||||||
stroke: string;
|
|
||||||
strokeWidth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_VECTOR_CONFIG: VectorConfig = {
|
|
||||||
kind: 'rect',
|
|
||||||
svgContent: '',
|
|
||||||
path: '',
|
|
||||||
tileWidth: 50,
|
|
||||||
tileHeight: 50,
|
|
||||||
fill: '#409EFF',
|
|
||||||
stroke: '#303133',
|
|
||||||
strokeWidth: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const VECTOR_CONFIG_KEYS: Array<keyof VectorConfig> = [
|
|
||||||
'kind',
|
|
||||||
'svgContent',
|
|
||||||
'path',
|
|
||||||
'tileWidth',
|
|
||||||
'tileHeight',
|
|
||||||
'fill',
|
|
||||||
'stroke',
|
|
||||||
'strokeWidth'
|
|
||||||
];
|
|
||||||
|
|
||||||
type FrameRequest = (callback: FrameRequestCallback) => number;
|
|
||||||
type FrameCancel = (handle: number) => void;
|
|
||||||
|
|
||||||
const getDefaultRequestFrame = (): FrameRequest | undefined =>
|
|
||||||
typeof globalThis.requestAnimationFrame === 'function'
|
|
||||||
? globalThis.requestAnimationFrame.bind(globalThis)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const getDefaultCancelFrame = (): FrameCancel | undefined =>
|
|
||||||
typeof globalThis.cancelAnimationFrame === 'function'
|
|
||||||
? globalThis.cancelAnimationFrame.bind(globalThis)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
export function buildNextVectorConfig(
|
|
||||||
current: VectorConfig,
|
|
||||||
incoming?: Record<string, any> | null
|
|
||||||
): VectorConfig | null {
|
|
||||||
if (!incoming || typeof incoming !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
const next = { ...current };
|
|
||||||
|
|
||||||
for (const key of VECTOR_CONFIG_KEYS) {
|
|
||||||
if (!(key in incoming)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const incomingValue = incoming[key];
|
|
||||||
if (incomingValue === undefined || incomingValue === current[key]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
(next as Record<string, any>)[key] = incomingValue;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed ? next : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRafLatestScheduler<T>(
|
|
||||||
apply: (payload: T) => void,
|
|
||||||
options?: {
|
|
||||||
requestFrame?: FrameRequest;
|
|
||||||
cancelFrame?: FrameCancel;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const requestFrame = options?.requestFrame ?? getDefaultRequestFrame();
|
|
||||||
const cancelFrame = options?.cancelFrame ?? getDefaultCancelFrame();
|
|
||||||
|
|
||||||
let rafId: number | null = null;
|
|
||||||
let pendingPayload: T | null = null;
|
|
||||||
|
|
||||||
const flush = () => {
|
|
||||||
if (pendingPayload === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const latestPayload = pendingPayload;
|
|
||||||
pendingPayload = null;
|
|
||||||
apply(latestPayload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const schedule = () => {
|
|
||||||
if (!requestFrame) {
|
|
||||||
flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (rafId !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rafId = requestFrame(() => {
|
|
||||||
rafId = null;
|
|
||||||
flush();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
enqueue(payload: T) {
|
|
||||||
pendingPayload = payload;
|
|
||||||
schedule();
|
|
||||||
},
|
|
||||||
flush,
|
|
||||||
cancel() {
|
|
||||||
if (rafId !== null && cancelFrame) {
|
|
||||||
cancelFrame(rafId);
|
|
||||||
}
|
|
||||||
rafId = null;
|
|
||||||
pendingPayload = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user