Skip to content

fix: rebuild background on theme+background update, and release word cloud shape hook - #4679

Merged
xile611 merged 3 commits into
VisActor:developfrom
g1f9:fix/background-recompile-and-wordcloud-tap
Sep 14, 2026
Merged

fix: rebuild background on theme+background update, and release word cloud shape hook#4679
xile611 merged 3 commits into
VisActor:developfrom
g1f9:fix/background-recompile-and-wordcloud-tap

Conversation

@g1f9

@g1f9 g1f9 commented Sep 13, 2026

Copy link
Copy Markdown

🤔 This is a ...

  • Bug fix

🔗 Related issue link

fix #4649

🔗 Related PR link

🐞 Bugserver case id

💡 Background and solution

Two independent crashes/regressions, both reproduced by the unit tests added here (all of them fail on develop and pass with this PR).

1. updateSpec ignores background when theme changes in the same call — #4649

_updateSpec only compared background inside the else branch of the theme check:

if (!isEqual(lastSpec.theme, this._spec.theme)) {
  result.changeTheme = true;
} else if (!isEqual(this._spec.background, lastSpec.background)) {
  result.reMake = true;
  result.changeBackground = true;
}

So a call that changes both produced changeTheme: true with neither reMake nor changeBackground. stage.background was still updated indirectly through _setCurrentTheme(), which is why the stage looked correct while the canvas did not change: the background mark built from the previous { fill } spec was never rebuilt and kept covering the whole viewBox.

The two checks are independent, so they are now two separate ifs. Changing background alone already implied reMake, so this only makes the combined case behave like the background-only case.

2. Cannot read properties of undefined (reading 'group') when recompiling a mark-spec background — the "additional observation" in #4649

BaseMark._compileProduct guards option on two lines but dereferences it bare on a third:

this._context = option?.context;          // guarded
...
if (option.group && product.parent !== option.group) { ... }   // NOT guarded
...
this._initProduct(option?.group);         // guarded

BaseChart.compileBackground() is the one caller that passes no option (this._backgroundMark.compile()). A chart whose background is a mark spec therefore throws on the second compile — the reCompile without reMake path, e.g. when only the legend changes — because the background mark's product already exists and execution reaches the unguarded branch. The first render is always safe, which is why this only shows up on update.

3. Word cloud shape leaves a stale afterRender tap behind

The word cloud shape layout is asynchronous. When it finishes, onLayoutFinished pushes an afterWordcloudShapeDraw tap onto stage.hooks.afterRender, and that tap only unregisters itself once it is actually called:

onLayoutFinished: () => {
  const afterWordcloudShapeDraw = () => {
    this._option.globalInstance.getStage().hooks.afterRender.taps = ... // self-removal
  };
  this._option.globalInstance.getStage().hooks.afterRender.taps.push({ ... });
}

If the series is released in that window — any updateSpec that triggers a reMake — CompilableBase.release() sets _option to null, and the next stage.render() runs the stale tap and throws Cannot read properties of null (reading 'globalInstance') from inside the hook, aborting the whole render. The tap also stays registered forever when it is never called.

The series now keeps a reference to the registered tap, removes it in release() and before registering a new one, and the tap body tolerates a released series.

📝 Changelog

Language Changelog
🇺🇸 English Fix background not being re-rendered when spec.background and spec.theme change in the same updateSpec call; fix a crash when recompiling a chart whose background is a mark spec; fix a crash and a leaked afterRender hook when a word cloud shape series is released while its layout is still running.
🇨🇳 Chinese 修复同一次 updateSpec 中同时修改 spec.backgroundspec.theme 时背景不刷新的问题;修复图表 background 为 mark 配置时二次编译崩溃的问题;修复形状词云布局未结束就释放 series 导致的崩溃与 afterRender 钩子泄漏。

☑️ Self-Check before Merge

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • TypeScript definition is updated/provided or not needed
  • Changelog is provided or not needed

…gether

`updateSpec` compared `background` only in the `else` branch of the theme check,
so a call that changes both `spec.background` and `spec.theme` produced
`changeTheme: true` without `reMake` / `changeBackground`. `stage.background` was
still updated through `_setCurrentTheme()`, but the background mark built from the
previous `{ fill }` spec was never rebuilt and kept covering the whole viewBox, so
nothing changed on the canvas.

`BaseMark._compileProduct` also dereferenced `option.group` bare while the two
other uses of `option` in the same method are guarded. `BaseChart.compileBackground()`
calls `this._backgroundMark.compile()` with no option, so a chart whose `background`
is a mark spec throws `Cannot read properties of undefined (reading 'group')` on the
second compile (`reCompile` without `reMake`), for example when only the legend
changes.

fix VisActor#4649
…eased

The word cloud shape layout is asynchronous. When it finishes, `onLayoutFinished`
pushes an `afterWordcloudShapeDraw` tap onto `stage.hooks.afterRender`, and that
tap only unregisters itself once it is actually called. If the series is released
in between — any `updateSpec` that triggers a reMake — `CompilableBase.release()`
sets `_option` to `null`, and the next render throws
`Cannot read properties of null (reading 'globalInstance')` from inside the hook,
which aborts the whole render.

Keep a reference to the registered tap on the series, remove it in `release()` and
before registering a new one, and let the tap body tolerate a released series.
@xuefei1313

Copy link
Copy Markdown
Contributor

🦞 Aime Bot Review

感谢贡献!这个 PR 一次修复了三个相互独立的问题,描述清晰,且每个修复都配了可复现的单测,质量很高 👍

改动摘要

  1. vchart.ts_updateSpec 中把 background 判断从 theme 的 else if 拆成独立 if,修复同一次 updateSpec 同时改 theme + background 时背景 mark 不重建的问题([Bug] updateSpec: background is not re-rendered when spec.background and spec.theme change in the same call #4649)。
  2. base-mark.ts_compileProduct 中未传 option(chart 级 background mark 二次 compile)时的空指针崩溃,改为 option?.group
  3. word-cloud/base.ts:形状词云异步布局挂到 stage.hooks.afterRender 上的 tap 改为随 series 生命周期管理,修复 layout 未完成时 reMake/release 导致的崩溃与钩子泄漏。

代码层面的观察

1. vchart.ts(核心修复,逻辑正确)
拆分后 changeThemereMake + changeBackground 可以同时为 true,语义上两者本就独立,修复方式最小且正确。请确认下游消费 IUpdateSpecResult 的路径(theme 切换的 reInit 与 background 的 reMake)在同一次更新中同时触发时不会互相打架——从现有代码看 background 走 reMake 会整体重建,理论上是安全的,但建议结合新增单测在浏览器里实际回归一次主题+背景同改的场景。

2. base-mark.ts
option?.group 与同一函数内已有的 option?.context_initProduct(option?.group) 守卫风格保持一致,属于精准的防御性修复。无 option 时保持既有 parent 不动也符合 compileBackground() 的调用语义,没有问题。

3. word-cloud/base.ts(设计稳妥)

  • tap 描述里保存了 stage 引用,即使 series release 后 _option 被置空,release() 中依然能从旧 stage 上摘除 tap,这个细节考虑得很到位;
  • 每次 onLayoutFinished 先摘除旧 tap 再注册,避免布局多次回调时 tap 重复挂载;
  • tap 执行体内对 released series(globalInstance 为空)做了容错,避免「tap 被别处留存但 series 已释放」时抛错。

一个小的可选建议(非阻塞):tap 的摘除依赖 fn 引用相等,目前实现是正确的;后续如果这类「挂到 stage 上的临时 tap」变多,可以考虑抽一个通用的 disposable tap 工具,避免各 series 各自维护。

测试

  • update-spec.test.ts 新增用例分别断言了 theme+background 同改时 changeTheme/changeBackground/reMake 同时成立,以及 mark spec 背景在 reCompile(只改 legend)路径下不抛错,覆盖与修复点一一对应;
  • 新增的 word-cloud-shape-release.test.ts 覆盖了 tap 随 release 摘除、tap 体容错、重复 layout 不重复注册三个关键点。通过私有方法驱动属于白盒测试,但用于验证生命周期边界问题是合理的。

测试覆盖充分,无需补充。

合并建议

三个修复彼此独立、diff 克制、测试齐全,建议在 CI 全部通过后合并。当前 PR 状态为 blocked(等待检查),请确认 CI 绿灯;changeset(common/changes 下的 patch 记录)本 PR 已携带 ✅。

再次感谢!🎉

@xile611 xile611 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@g1f9 感谢修复!我将当前提交 547eaecdf496778b5a6dff25078769ebbe20cb6f 应用到 develop c07b139096287461be2648e2aaeae9be563925ac,使用 VRender 1.1.8 做了验证,未发现阻塞性的正确性问题,整体可以推进合并。

三个修复点均已验证:

  • 同时修改 theme 与 background 后,新背景的实际画布像素正确,旧背景 mark 不再覆盖新背景;覆盖了渐变转纯色、纯色转 mark、mark 转另一个 mark,以及移除显式背景后使用 theme 背景。
  • mark 形式的背景在只改图例、触发二次编译时不再崩溃,背景像素保持正确。
  • 词云 series 释放或 updateSpec 重建时,待执行的钩子会被清理;释放后到达的布局完成回调不会重新注册钩子;正常绘制完成事件只触发一次并清理自身。

本地结果:现有两组测试共 55 项通过,补充的公共 updateSpec、画布像素与生命周期测试 8 项通过

合并前麻烦处理一处工程规范调整:钩子的注册与移除请使用 VRender 已有的 tap(name, fn) / unTap(name, fn),替换直接修改 taps 数组的实现,详见行内评论。项目已有标准接口能表达同一行为,应优先复用;这里不需要额外抽象。请保留函数引用,确保只移除当前 series 的回调。

Comment thread packages/vchart/src/series/word-cloud/base.ts Outdated
Use the standard `SyncHook.tap` / `SyncHook.unTap` API instead of pushing to and
filtering `hooks.afterRender.taps` by hand, matching how `Compiler` cleans up its
own stage hooks. `unTap` keeps the `fn` argument so a stage hosting more than one
word cloud series only drops its own callback.
@g1f9

g1f9 commented Sep 14, 2026

Copy link
Copy Markdown
Author

Thanks — switched to the standard hook API in eebadbc.

// register
stage.hooks.afterRender.tap('afterWordcloudShapeDraw', afterWordcloudShapeDraw);

// remove, using the saved stage and fn
tap.stage?.hooks?.afterRender?.unTap('afterWordcloudShapeDraw', tap.fn);

The fn argument is kept as you suggested, so a stage hosting more than one word cloud series only drops its own callback. The existing { stage, fn } reference on the series is unchanged — no extra helper was introduced.

Unit tests still pass (word-cloud-shape-release.test.ts and the two background cases in update-spec.test.ts), and tsc --noEmit is clean.

@xile611
xile611 merged commit 0461971 into VisActor:develop Sep 14, 2026
3 of 6 checks passed
xile611 pushed a commit that referenced this pull request Sep 14, 2026
Follow-up to #4679.

`hooks.afterRender` is declared as `SyncHook<[IStage]>`, so a tap already receives
the stage it runs on — the same way `Stage` registers its own `constructor` tap.
That removes the need to remember `{ stage, fn }` on the series: the callback is a
member with a stable identity, it unregisters itself from the stage it is handed,
and `release()` reaches the stage through `_option`, which is still alive before
`super.release()` clears it.

Drops the `_afterWordcloudShapeDrawTap` field and the
`_removeAfterWordcloudShapeDrawTap` helper, and moves the tap name into a single
module constant so registration and removal cannot drift apart.

No behavior change: the existing word cloud tap tests still cover removal on
release and the absence of duplicate registrations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] updateSpec: background is not re-rendered when spec.background and spec.theme change in the same call

4 participants