Skip to content

feat: taro 支持 ttdom#18850

Open
xzdadada wants to merge 1 commit intoNervJS:mainfrom
xzdadada:feat/tt-ttdom
Open

feat: taro 支持 ttdom#18850
xzdadada wants to merge 1 commit intoNervJS:mainfrom
xzdadada:feat/tt-ttdom

Conversation

@xzdadada
Copy link
Copy Markdown

@xzdadada xzdadada commented Feb 6, 2026

这个 PR 做了什么? (简要描述所做更改)

一、概述

官方提供 tt-dom 接口,让 taro 视图创建/更新更加高效。
通过 tt-dom 接口,tarojs 可以不依赖于 Template 进行递归渲染节点。

二、技术方案

TT-DOM 架构

整体架构将从 react -> taro dom -> setData -> template render 变成 react -> tt-dom -> 内置组件 render。
20260210-144635

接入 TT-DOM

抖音小程序用户可通过在 appConfig 中配置 enableTTDom: true 开启 tt-dom
架构内,判断开启 enableTTDom 并且使用抖音小程序时,在入口文件中注入 tt.$enableTTDom$ = true,在运行时提供 isEnableTTDom 判断,却别处理逻辑

三、tt-dom 使用示例

onLoad() {
  /**
   * <view class="red">this is tt-dom render</view>
   */

  // 创建
  const { appDocument } = tt;
  const view = appDocument.createElement('view');
  const content = appDocument.createTextNode(`this is tt-dom render`);

  // 设置属性
  view.setAttribute('class', 'red');

  // 添加子节点
  view.appendChild(content);

  // 挂载到根节点
  const pageDocument = appDocument.getPageDocumentById(this.__webviewId__);
  pageDocument.appendChild(view);
 }

四、收益

  • 优化点一:简化 React 使用的 Dom 接口,优化 DOM 相关的数据结构,去除调用 setData 接口过程。React 直接对接到 tt-dom 接口,直接发送指令,将数据更新的粒度由页面细化到节点,提升更新性能。
  • 优化点二:Template 渲染性能较差,主要原因其一是 Template 在实现会多创建 Fragment 节点,在 tt-dom 后不存在。其二 Template 渲染是递归渲染,性能较差。
    测试数据:
    基准 Benchmark:借鉴 https://github.com/krausest/js-framework-benchmark 中对于框架的性能测试方案,对于创建、更新、交换、删除四个场景在 ios 真机环境下进行对比测试,得到如下数据结果
微信图片_20260225175542_14_148

这个 PR 是什么类型? (至少选择一个)

  • 错误修复 (Bugfix) issue: fix #
  • 新功能 (Feature)
  • 代码重构 (Refactor)
  • TypeScript 类型定义修改 (Types)
  • 文档修改 (Docs)
  • 代码风格更新 (Code style update)
  • 构建优化 (Chore)
  • 其他,请描述 (Other, please describe):

这个 PR 涉及以下平台:

  • 所有平台
  • Web 端(H5)
  • 移动端(React-Native)
  • 鸿蒙(Harmony)
  • 鸿蒙容器(Harmony Hybrid)
  • ASCF 元服务
  • 快应用(QuickApp)
  • 所有小程序
  • 微信小程序
  • 企业微信小程序
  • 京东小程序
  • 百度小程序
  • 支付宝小程序
  • 支付宝 IOT 小程序
  • 钉钉小程序
  • QQ 小程序
  • 飞书小程序
  • 快手小程序
  • 头条小程序

Summary by CodeRabbit

  • 新功能

    • 可选启用抖音小程序 TT DOM 渲染模式,调整渲染/更新与事件分发、内联 HTML 处理以适配 TT 环境。
    • 扩展内置组件白名单与无单位 CSS 属性集合,支持样式对象到 CSS 字符串的转换,提升样式兼容性。
    • 新增渲染模式枚举与运行时检测接口以优化分支行为。
  • 配置

    • 在应用配置中新增 enableTTDom 开关,并在构建链路中传递该配置以控制运行时行为。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

新增针对字节跳动(tt)小程序的可选 TT Dom 渲染支持:添加共享常量与检测工具、编译时注入 enableTTDom 配置、运行时通过 isEnableTTDom 分流为 TT Dom 的 document/element/event/innerHTML/样式处理逻辑,并暴露相关运行时接口。

Changes

Cohort / File(s) Summary
共享常量
packages/shared/src/constants.ts
新增并导出 TT_SPECIFIC_COMPONENTSDEFAULT_COMPONENTSUNITLESS_PROPERTIES_SET 三个 Set 常量。
共享工具
packages/shared/src/utils.ts
新增 TTRenderType 枚举与 isEnableTTDom()(环境与 tt.getRenderMode 检测,模块级缓存)。
React props / 事件 适配
packages/taro-react/src/props.ts
在 TARO_ENV === 'tt' 且 TT Dom 启用时:事件绑定改为 bind{event} 形式并缓存绑定函数;style 对象通过 styleObjectToCss 转为字符串并用 setAttribute('style', ...);dangerouslySetInnerHTML 调用 setInnerHTML;非 dataset/aria 属性在 TT 分支使用 camelCase。
Document 与 innerHTML
packages/taro-runtime/src/bom/document.ts, packages/taro-runtime/src/dom-external/inner-html/html.ts
新增 createTTDomDocument(),在 taroDocumentProvider 中基于 isEnableTTDom() 选择 TT Dom 文档或回退;setInnerHTML 在 TT 环境下可使用 tt.appDocument 作为 ownerDocument。
事件处理与导出
packages/taro-runtime/src/dom/event.ts, packages/taro-runtime/src/index.ts
新增并导出 eventHandlerTTDom()(为 TT Dom 事件添加 mpEvent/bubbles/cancelable 并调用 listener);runtime index 同时新增 setInnerHTML 的 re-export。
DSL 与生命周期适配
packages/taro-runtime/src/dsl/common.ts, packages/taro-runtime/src/interface/hydrate.ts
在创建页面/组件时,TT Dom 启用时改用 getPageDocumentById/sync() 等 TT 特殊路径,并在若干生命周期跳过原有 performUpdate;为 MpInstance 添加可选 __webviewId__?: number 字段。
构建器注入与运行时代码
packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts, packages/taro-webpack5-runner/src/plugins/TaroLoadChunksPlugin.ts, packages/taro-webpack5-runner/src/utils/webpack.ts
appConfig 传入 TaroLoadChunksPluginTaroLoadChunksPlugin 接收并保存 appConfigaddRequireToSource 新增可选 appConfig 参数,并在 TARO_ENV === 'tt'appConfig.enableTTDom 为真时向生成源码注入对 tt.__$enableTTDom$__ 的赋值。
类型声明
packages/taro/types/taro.config.d.ts
AppConfig 接口中新增可选布尔属性 enableTTDom?: boolean

Sequence Diagram(s)

sequenceDiagram
    participant Webpack as Webpack (编译)
    participant MiniPlugin as MiniPlugin
    participant TaroLoadChunks as TaroLoadChunksPlugin
    participant AppSource as 生成源码 (webpack util)
    participant Runtime as Taro Runtime
    participant TTEnv as tt (运行时 / appDocument)

    Webpack->>MiniPlugin: 传入 options(包含 appConfig)
    MiniPlugin->>TaroLoadChunks: 传递 appConfig
    TaroLoadChunks->>AppSource: addRequireToSource(..., appConfig)
    alt TARO_ENV === 'tt' 且 appConfig.enableTTDom
        AppSource->>AppSource: 注入 tt.__$enableTTDom$__ = appConfig.enableTTDom
    end
    Runtime->>Runtime: 调用 isEnableTTDom()
    alt isEnableTTDom() 为 true
        Runtime->>TTEnv: 使用 tt.appDocument 创建 TT Dom(createTTDomDocument)
        Runtime->>TTEnv: 使用 eventHandlerTTDom 派发事件(bind*/catch* 适配)
        Runtime->>TTEnv: styleObjectToCss -> setAttribute('style', ...)
        Runtime->>TTEnv: setInnerHTML 使用 tt.appDocument 解析并设置内容
    else
        Runtime->>Runtime: 使用常规 createDocument()/performUpdate 流程
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • luckyadam
  • yoyo837
  • tutuxxx
  • ZakaryCode

Poem

🐰 TTDom 新装上线啦,
编译插旗静悄悄,
运行时分支稳又巧,
事件样式都照料,innerHTML 不慌张,
小兔跳跃把改动唱!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰简洁地总结了主要变更:为 Taro 框架添加 ttdom 支持。标题与所有文件变更高度相关,准确反映了该 PR 的核心目标。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@packages/taro-react/src/props.ts`:
- Around line 243-249: In the TT DOM branch inside the block guarded by
isEnableTTDom() handle the case where value is null/undefined (currently neither
isString nor isObject) and clear the old styles; update the logic around the
checks for isString(value)/isObject(value) in that branch (the code using
dom.setAttribute('style'...) and styleObjectToCss) to call the DOM clearing API
(e.g., dom.removeAttribute('style') or set an empty style) when value == null ||
value === undefined so old styles are removed in the TT path.

In `@packages/taro-runtime/src/bom/document.ts`:
- Around line 50-51: createTTDomDocument currently accesses tt.appDocument
without null/undefined checks; add a defensive guard (use optional chaining or
an explicit null check) around tt and tt.appDocument in createTTDomDocument and
either return a safe fallback or throw a clear, descriptive error so failures
are informative (align style with usage at tt?.getBuiltInComponents). Locate the
createTTDomDocument function and update its access to tt.appDocument to validate
tt and tt.appDocument before use, and ensure any thrown error message mentions
"tt.appDocument" for easier debugging.
- Line 69: The value returned from tt?.getBuiltInComponents?.() may not be a
Set, so initialize builtInComponents defensively: call
tt?.getBuiltInComponents?.(), check with instanceof Set (or Array.isArray) and
if it's not a Set convert it to one (e.g., new Set(returnedValue) or fallback to
new Set([...DEFAULT_Components, ...TT_SPECIFIC_COMPONENTS])); update the code
around the builtInComponents initialization and ensure later uses like
builtInComponents.has(type) are safe.
- Around line 106-107: The function createTTDomDocument currently returns
tt.appDocument typed as any, which lets an any flow into taroDocumentProvider
typed TaroDocument; update createTTDomDocument to declare an explicit return
type compatible with TaroDocument (or a narrower interface) and ensure its
implementation returns a value matching that type (replace the implicit any on
tt.appDocument with a typed wrapper or cast within createTTDomDocument) so the
compiler can validate that taroDocumentProvider (and env.document) adhere to
TaroDocument.

In `@packages/taro-runtime/src/interface/hydrate.ts`:
- Line 15: The __webviewId__ property on the MpInstance interface is currently
required but only used for the TT DOM path; make it optional (like the existing
route? pattern) so non-TT platforms don't need to provide it—locate the
MpInstance declaration in hydrate.ts and change the __webviewId__ member to an
optional property (__webviewId__?: number) so TypeScript won't force it for all
implementations.
🧹 Nitpick comments (8)
packages/taro-webpack5-runner/src/utils/webpack.ts (1)

17-22: enableTTDom 未设置时,注入了不必要的代码。

当用户没有在配置中设置 enableTTDom 时,appConfig.enableTTDomundefined,生成的代码将会是 tt.__$enableTTDom$__ = undefined;。虽然运行时不会出错(undefined 是 falsy),但这段代码对不使用 TT DOM 的抖音小程序来说是多余的。

建议仅在 enableTTDomtrue 时注入,或至少确保输出的是布尔值:

♻️ 建议的修改
-  if (process.env.TARO_ENV === 'tt' && appConfig) {
+  if (process.env.TARO_ENV === 'tt' && appConfig?.enableTTDom) {
     source.add(`
 if (typeof tt !== 'undefined') {
-  tt.__$enableTTDom$__ = ${appConfig.enableTTDom};
+  tt.__$enableTTDom$__ = true;
 }\n`)
   }
packages/shared/src/utils.ts (1)

275-283: styleObjectToCss 的类型签名中 ArrayLike<unknown> 不太合理

函数接受 ArrayLike<unknown> 类型,但内部使用 Object.entries(style) 处理,这会将 ArrayLike 的索引作为 CSS 属性名(如 "0", "1"),产生无效的 CSS。如果调用方始终传入对象,建议收窄类型。

♻️ 建议收窄类型签名
-export function styleObjectToCss(style: { [s: string]: unknown } | ArrayLike<unknown>) {
+export function styleObjectToCss(style: Record<string, string | number>) {
packages/taro-runtime/src/dom-external/inner-html/html.ts (1)

30-34: typeof tt !== 'undefined' 检查冗余

isEnableTTDom() 内部已经检查了 typeof tt === 'undefined' 并在该情况下返回 false。因此当代码执行到 Line 31 时,tt 必定已定义,该检查可以简化。

♻️ 简化冗余检查
  if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
-   if (typeof tt !== 'undefined' && 'appDocument' in tt) {
+   if ('appDocument' in tt) {
      ownerDocument = tt.appDocument
    }
  }
packages/taro-runtime/src/dom/event.ts (1)

206-213: event.mpEvent = event 产生循环引用

Line 208 将 mpEvent 设置为 event 自身,产生循环引用。虽然这可能是为了让 TT DOM 事件对象兼容 TaroEventmpEvent 访问模式,但循环引用可能导致序列化(如 JSON.stringify)抛异常,或在调试时带来困扰。建议添加注释说明此设计意图。

📝 建议添加注释说明
 export function eventHandlerTTDom(ele: any, listener: (event: MpEvent, element: any) => void, event: MpEvent) {
+  // Note: mpEvent 指向 event 自身以兼容 TaroEvent.mpEvent 的访问模式
   Object.assign(event, {
     mpEvent: event,
     bubbles: true,
     cancelable: true,
   })
   listener(event, ele)
 }
packages/taro-react/src/props.ts (1)

129-138: eventHandlerTTDom.bind(this, dom, value)this 的值不确定

setEvent 是一个普通函数而非方法,this 在严格模式下为 undefined。虽然 eventHandlerTTDom 内部未使用 this,但为避免歧义,建议使用 null 替代。

♻️ 建议修复
-      dom[`__${eventName}__`] = eventHandlerTTDom.bind(this, dom, value)
+      dom[`__${eventName}__`] = eventHandlerTTDom.bind(null, dom, value)
packages/shared/src/constants.ts (1)

41-41: DEFAULT_Components 的命名不符合该文件的常量命名约定

该文件中所有其他常量均使用 UPPER_SNAKE_CASE(如 COMPILE_MODE_IDENTIFIER_PREFIXPLATFORM_CONFIG_MAPTT_SPECIFIC_COMPONENTS),此处应保持一致性。

♻️ 建议重命名为 DEFAULT_COMPONENTS
-export const DEFAULT_Components = new Set<string>([
+export const DEFAULT_COMPONENTS = new Set<string>([

需要同步更新 packages/taro-runner-utils/src/constant.ts 中的定义以及所有引用处。

packages/taro-runtime/src/bom/document.ts (2)

71-78: 使用 __proto__ 访问原型方法是已弃用的模式。

虽然已有 eslint-disable 注释,但 __proto__ 在严格模式下行为不可靠,且属于已弃用特性。建议使用 Object.getPrototypeOf(this) 替代。

建议:使用 Object.getPrototypeOf 替代 __proto__
   document.getElementById = function getElementById(id: string) {
     if (id === 'app') {
       return app
     } else {
-      // eslint-disable-next-line no-proto
-      return this.__proto__.getElementById.call(this, id)
+      return Object.getPrototypeOf(this).getElementById.call(this, id)
     }
   }

createElement 内部同理。

Also applies to: 84-96


80-82: getLastPage 的实现将整个 Map 展开为数组,仅为取最后一个元素。

可以使用迭代器避免 O(n) 的数组分配,虽然对于小 Map 影响有限。

更高效的替代方案
   document.getLastPage = function getLastPage() {
-    return [...this._pageDocumentMap.values()][this._pageDocumentMap.size - 1]
+    let last
+    for (const v of this._pageDocumentMap.values()) last = v
+    return last
   }

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/taro-runtime/src/dsl/common.ts (1)

374-391: ⚠️ Potential issue | 🟡 Minor

TTDom 模式下 customWrapperCache 的不一致性问题。

ATTACHEDDETACHED 生命周期在 TTDom 启用时直接返回,导致 customWrapperCache 不被填充和清理。但在 root.ts 的数据更新路径中(performUpdate() 中的 findCustomWrapper() 调用),仍会尝试从这个空缓存中读取自定义包裹组件,此代码路径缺少 TTDom 守卫。

当 TTDom 启用时,指向自定义包裹组件的数据更新将无法从缓存中找到对应组件,导致这些更新被误认为是页面级别的更新而不是被正确路由到 customWrapperMap。请确认 TTDom 是否不支持自定义包裹组件的数据更新,或在数据更新路径中添加相应的 TTDom 守卫。

🤖 Fix all issues with AI agents
In `@packages/shared/src/utils.ts`:
- Around line 275-283: In styleObjectToCss, filter out entries whose value is
null or undefined before mapping to CSS so you don't emit "key: undefined|null;"
strings; update the code that iterates Object.entries(style) to skip entries
where value === null || value === undefined, then continue using kebabCaseKey
and isUnitlessProperty to produce the final cssValue and string.

In `@packages/taro-react/src/props.ts`:
- Around line 129-138: 在 TT DOM 分支里将 eventHandlerTTDom 的绑定上下文由 this 显式改为 null:把对
eventHandlerTTDom.bind 的调用(目前为 eventHandlerTTDom.bind(this, dom, value))改为
eventHandlerTTDom.bind(null, dom, value),同时保持对 dom[`__${eventName}__`] 的赋值与
addEventListener 调用不变;参考相关符号:setEvent(由 setProperty
普通调用),eventHandlerTTDom,dom[`__${eventName}__`],addEventListener,以确保意图明确且不依赖
this 上下文。

In `@packages/taro-runtime/src/dsl/common.ts`:
- Around line 164-168: The TTDom branch is dropping the callback `cb` (passed
from ONLOAD) by calling `(pageElement as any).sync()` without invoking `cb`,
which can break lifecycle signaling; update the TTDom path so that after calling
`pageElement.sync()` you invoke the same `cb` (guarding for existence) just like
the non-TTDom path uses `pageElement.performUpdate(true, cb)`; ensure you call
`cb()` after sync completes (synchronously if sync is sync) or schedule it
appropriately if `sync()` returns/accepts async completion.
🧹 Nitpick comments (7)
packages/taro-webpack5-runner/src/utils/webpack.ts (1)

5-5: AppConfig 仅作为类型使用,应使用 import type

AppConfig 在第 15 行仅用作参数类型注解,而非运行时值。当前使用值导入(value import)可能导致打包时引入不必要的运行时代码,且与第 8 行 webpack 类型的 import type 风格不一致。

♻️ 建议修改
-import { AppConfig } from '@tarojs/taro'
+import type { AppConfig } from '@tarojs/taro'
packages/taro-runtime/src/dsl/common.ts (2)

153-157: TTDom 路径下 pageElement 的类型安全性较弱。

(env.document as any).getPageDocumentById(this.__webviewId__) 返回的对象被赋给 pageElement: TaroRootElement | null,但实际类型可能与 TaroRootElement 不同。后续代码(如 Line 159 的 ensure 检查、Line 163 的 ctx 赋值)都依赖于它是 TaroRootElement。建议为 TTDom 的 page document 补充类型定义或至少加一个接口声明,避免在后续维护中出现隐式类型不匹配的问题。


153-157: 可选优化:提取重复的 TTDom 判断条件。

process.env.TARO_ENV === 'tt' && isEnableTTDom() 在本文件中出现了 5 次。虽然外层的 TARO_ENV 检查对编译时 dead-code elimination 有意义(process.env.TARO_ENV 会在构建时被替换),但建议在首次出现时加一行注释说明这一设计意图,方便后续维护者理解为什么 isEnableTTDom() 内部已有相同检查仍要在外部重复。

Also applies to: 164-168, 335-337, 374-377, 388-391

packages/shared/src/utils.ts (2)

268-272: 三元赋值可简化为布尔表达式。

♻️ 建议简化
   const ttMode = tt.getRenderMode ? tt.getRenderMode() : TTRenderType.V1
-
-  ttMode === TTRenderType.V2 && tt.__$enableTTDom$__ ? (ttUseV2TTDom = true) : (ttUseV2TTDom = false)
+  ttUseV2TTDom = ttMode === TTRenderType.V2 && !!tt.__$enableTTDom$__
 
   return ttUseV2TTDom

261-265: isEnableTTDom 在非 TT 环境下每次调用都会重复检查环境变量。

TARO_ENV !== 'tt' 时,函数每次都会走到 Line 263 的 return false,而不会利用 ttUseV2TTDom 缓存。虽然性能影响很小,但若此函数在热路径中被频繁调用(如 setPropertysetEvent 等),可以考虑也缓存非 TT 环境的结果。

packages/taro-runtime/src/bom/document.ts (2)

88-90: getLastPage 将整个 Map 展开为数组只为取最后一个元素,效率较低。

[...this._pageDocumentMap.values()][this._pageDocumentMap.size - 1] 会创建一个临时数组,当页面栈较深时有不必要的内存分配。可以改用迭代器直接获取最后一个值。

♻️ 建议优化
   document.getLastPage = function getLastPage() {
-    return [...this._pageDocumentMap.values()][this._pageDocumentMap.size - 1]
+    let last
+    for (const v of this._pageDocumentMap.values()) last = v
+    return last
   }

79-86: 使用 __proto__ 访问原型方法已被废弃,建议使用 Object.getPrototypeOf

Line 84、98、100 处使用了 this.__proto__ 来调用原型方法。虽然已有 eslint-disable 注释,但 __proto__ 是非标准的遗留特性,推荐使用 Object.getPrototypeOf(this) 替代。

♻️ 建议替换示例(getElementById 为例)
   document.getElementById = function getElementById(id: string) {
     if (id === 'app') {
       return app
     } else {
-      // eslint-disable-next-line no-proto
-      return this.__proto__.getElementById.call(this, id)
+      return Object.getPrototypeOf(this).getElementById.call(this, id)
     }
   }

createElement 中的 __proto__ 引用也应同样替换。

Also applies to: 92-109

This was referenced Feb 7, 2026
This was referenced Feb 28, 2026
@Single-Dancer Single-Dancer added this to the 4.1.12 milestone Apr 3, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
packages/taro-react/src/props.ts (2)

243-248: ⚠️ Potential issue | 🟠 Major

TT DOM 分支移除 style 时不会清空旧样式。

value 变成 null/undefined 时,这里既不 setAttribute 也不 removeAttribute,旧的 style 会一直残留。非 TT 分支已经会清空样式,两条路径现在行为不一致。

🔧 建议修复
     if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
       if (isString(value)) {
         dom.setAttribute('style', value)
       } else if (isObject(value)) {
         dom.setAttribute('style', styleObjectToCss(value as StyleValue))
+      } else if (value == null) {
+        dom.removeAttribute('style')
       }
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-react/src/props.ts` around lines 243 - 248, The TT DOM branch
in props.ts currently only sets style when value is string/object and never
clears it when value is null/undefined; update the branch inside the
isEnableTTDom() check (the block using isString(value), isObject(value),
dom.setAttribute and styleObjectToCss) to call dom.removeAttribute('style') when
value is nullish so old styles are cleared and behavior matches the non-TT
branch.

328-335: ⚠️ Potential issue | 🟡 Minor

styleObjectToCss 仍会把 undefined 序列化成无效 CSS。

Line 247 把任意对象强转成 StyleValue,运行时 { color: undefined } 仍然可能进来;当前会生成 color: undefined;

🔧 建议修复
 function styleObjectToCss(style: StyleValue) {
   return Object.entries(style)
+    .filter(([, value]) => value != null)
     .map(([key, value]) => {
       const kebabCaseKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
       const cssValue = typeof value === 'number' && !UNITLESS_PROPERTIES_SET.has(kebabCaseKey) ? `${value}px` : value === null ? '' : value
       return `${kebabCaseKey}: ${cssValue};`
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-react/src/props.ts` around lines 328 - 335, The
styleObjectToCss function currently serializes undefined (and null) values into
invalid CSS; update styleObjectToCss to skip entries whose value is undefined or
null before mapping (i.e., filter Object.entries(style) to remove v ===
undefined || v === null), then apply the existing kebab-case and
UNITLESS_PROPERTIES_SET logic so only valid values are emitted.
packages/taro-runtime/src/bom/document.ts (1)

166-186: ⚠️ Potential issue | 🔴 Critical

__eventWrappers 的清理方式对 WeakMap 无效,而且同一 listener 会覆盖不同事件的包装器。

__eventWrappers 在这里是 WeakMapdelete el.__eventWrappers[listener] 不会删除 WeakMap 条目。再加上当前只按 listener 建索引,同一个函数复用到多个事件时,后一次 set(listener, wrapper) 会覆盖前一个包装器,removeEventListener 很容易取错引用并留下悬挂监听。

🔧 建议修复
           if (!el.__eventWrappers) {
             el.__eventWrappers = new WeakMap()
           }
-          el.__eventWrappers.set(listener, wrapper)
+          const wrappers = el.__eventWrappers.get(listener) ?? new Map()
+          wrappers.set(bindEventName, wrapper)
+          el.__eventWrappers.set(listener, wrappers)
@@
-          const wrapper = el.__eventWrappers?.get(listener)
+          const wrappers = el.__eventWrappers?.get(listener)
+          const wrapper = wrappers?.get(bindEventName)
           if (wrapper) {
             ttRemoveEventListener(bindEventName, wrapper)
-            delete el.__eventWrappers[listener]
+            wrappers.delete(bindEventName)
+            if (wrappers.size === 0) {
+              el.__eventWrappers.delete(listener)
+            }
           }

可用下面的脚本直接确认这里先创建了 WeakMap,后面却用 delete obj[key] 清理,并且缓存键只有 listener

#!/bin/bash
rg -n -C3 '__eventWrappers\s*=\s*new WeakMap|__eventWrappers\.set\(listener,\s*wrapper\)|__eventWrappers\?\.get\(listener\)|delete\s+el\.__eventWrappers\[listener\]' packages/taro-runtime/src/bom/document.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-runtime/src/bom/document.ts` around lines 166 - 186, The code
creates el.__eventWrappers as a WeakMap but later tries to delete entries with
delete el.__eventWrappers[listener] and also only keys by listener (so one
wrapper overwrites another for different event types). Change the structure to a
WeakMap keyed by the listener that maps to a Map of bindEventName→wrapper:
initialize el.__eventWrappers = new WeakMap<Function, Map<string, Function>>(),
when adding do let m = el.__eventWrappers.get(listener) || new Map();
m.set(bindEventName, wrapper); el.__eventWrappers.set(listener, m); when
removing in removeEventListener, look up const m =
el.__eventWrappers.get(listener), get the wrapper via m.get(bindEventName), call
ttRemoveEventListener(bindEventName, wrapper) if found, m.delete(bindEventName)
and if m.size === 0 call el.__eventWrappers.delete(listener); this ensures
correct per-event wrappers and uses WeakMap.delete correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/taro-react/src/props.ts`:
- Around line 129-136: The TT DOM handler cache currently reuses
dom[`__${eventName}__`] for both capture and bubble handlers, causing onClick
and onClickCapture to override each other and incorrect removals; change the
cache key to include the capture flag (e.g. `__${eventName}__capture` vs
`__${eventName}__bubble` or append isCapture) so eventHandlerTTDom bindings are
stored separately, and ensure removeEventListener reads the same capture-aware
key before removing so addEventListener/removeEventListener use the same stored
function per (eventName,isCapture) pair.

In `@packages/taro-runtime/src/bom/document.ts`:
- Around line 114-135: The current setAttribute interceptor (function
setAttribute) only adds the emptyFunction listener when catchMove is truthy but
never removes it when catchMove is set to false, causing stale catchtouchmove
listeners; update setAttribute so when name === 'catchMove' and the new value is
falsy it calls el.removeEventListener('catchtouchmove', emptyFunction), and when
truthy it calls el.addEventListener('catchtouchmove', emptyFunction); keep
calling originalSetAttribute(name, value) and preserve the existing
removeAttribute behavior that removes the listener when the attribute is
deleted.

---

Duplicate comments:
In `@packages/taro-react/src/props.ts`:
- Around line 243-248: The TT DOM branch in props.ts currently only sets style
when value is string/object and never clears it when value is null/undefined;
update the branch inside the isEnableTTDom() check (the block using
isString(value), isObject(value), dom.setAttribute and styleObjectToCss) to call
dom.removeAttribute('style') when value is nullish so old styles are cleared and
behavior matches the non-TT branch.
- Around line 328-335: The styleObjectToCss function currently serializes
undefined (and null) values into invalid CSS; update styleObjectToCss to skip
entries whose value is undefined or null before mapping (i.e., filter
Object.entries(style) to remove v === undefined || v === null), then apply the
existing kebab-case and UNITLESS_PROPERTIES_SET logic so only valid values are
emitted.

In `@packages/taro-runtime/src/bom/document.ts`:
- Around line 166-186: The code creates el.__eventWrappers as a WeakMap but
later tries to delete entries with delete el.__eventWrappers[listener] and also
only keys by listener (so one wrapper overwrites another for different event
types). Change the structure to a WeakMap keyed by the listener that maps to a
Map of bindEventName→wrapper: initialize el.__eventWrappers = new
WeakMap<Function, Map<string, Function>>(), when adding do let m =
el.__eventWrappers.get(listener) || new Map(); m.set(bindEventName, wrapper);
el.__eventWrappers.set(listener, m); when removing in removeEventListener, look
up const m = el.__eventWrappers.get(listener), get the wrapper via
m.get(bindEventName), call ttRemoveEventListener(bindEventName, wrapper) if
found, m.delete(bindEventName) and if m.size === 0 call
el.__eventWrappers.delete(listener); this ensures correct per-event wrappers and
uses WeakMap.delete correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8ad01ec-6f0d-4911-a24a-ed1fd6500eb7

📥 Commits

Reviewing files that changed from the base of the PR and between 90f0c50 and 31503d4.

📒 Files selected for processing (13)
  • packages/shared/src/constants.ts
  • packages/shared/src/utils.ts
  • packages/taro-react/src/props.ts
  • packages/taro-runtime/src/bom/document.ts
  • packages/taro-runtime/src/dom-external/inner-html/html.ts
  • packages/taro-runtime/src/dom/event.ts
  • packages/taro-runtime/src/dsl/common.ts
  • packages/taro-runtime/src/index.ts
  • packages/taro-runtime/src/interface/hydrate.ts
  • packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts
  • packages/taro-webpack5-runner/src/plugins/TaroLoadChunksPlugin.ts
  • packages/taro-webpack5-runner/src/utils/webpack.ts
  • packages/taro/types/taro.config.d.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/taro-runtime/src/interface/hydrate.ts
  • packages/shared/src/constants.ts
  • packages/taro/types/taro.config.d.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/taro-runtime/src/dom-external/inner-html/html.ts
  • packages/taro-runtime/src/dom/event.ts
  • packages/taro-runtime/src/index.ts
  • packages/shared/src/utils.ts
  • packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts
  • packages/taro-runtime/src/dsl/common.ts

Comment on lines +129 to +136
if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
if (isFunction(oldValue)) {
(dom as any).removeEventListener(`bind${eventName}`, dom[`__${eventName}__`])
}
if (isFunction(value)) {
dom[`__${eventName}__`] = eventHandlerTTDom.bind(null, dom, value)
dom.addEventListener(`bind${eventName}`, dom[`__${eventName}__`], isCapture)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TT DOM 事件缓存键会让冒泡/捕获监听互相覆盖。

这里把缓存固定在 dom[\${eventName}`]onClickonClickCapture` 会落到同一个键上,后注册的会覆盖前一个;后续更新或卸载其中一个 prop 时,也会把另一个监听误删/遗留。

🔧 建议修复
 function setEvent (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) {
   const isCapture = name.endsWith('Capture')
+  const handlerKey = `__${name}__`
   let eventName = name.toLowerCase().slice(2)
   if (isCapture) {
     eventName = eventName.slice(0, -7)
   }
@@
   if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
     if (isFunction(oldValue)) {
-      (dom as any).removeEventListener(`bind${eventName}`, dom[`__${eventName}__`])
+      (dom as any).removeEventListener(`bind${eventName}`, dom[handlerKey], isCapture)
     }
     if (isFunction(value)) {
-      dom[`__${eventName}__`] = eventHandlerTTDom.bind(null, dom, value)
-      dom.addEventListener(`bind${eventName}`, dom[`__${eventName}__`], isCapture)
+      dom[handlerKey] = eventHandlerTTDom.bind(null, dom, value)
+      dom.addEventListener(`bind${eventName}`, dom[handlerKey], isCapture)
     }
     return
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-react/src/props.ts` around lines 129 - 136, The TT DOM handler
cache currently reuses dom[`__${eventName}__`] for both capture and bubble
handlers, causing onClick and onClickCapture to override each other and
incorrect removals; change the cache key to include the capture flag (e.g.
`__${eventName}__capture` vs `__${eventName}__bubble` or append isCapture) so
eventHandlerTTDom bindings are stored separately, and ensure removeEventListener
reads the same capture-aware key before removing so
addEventListener/removeEventListener use the same stored function per
(eventName,isCapture) pair.

Comment on lines +114 to +135
el.setAttribute = function (name: string, value: any) {
const result = originalSetAttribute(name, value)

// 处理 catchMove 属性
if (name === 'catchMove' && value) {
el.addEventListener('catchtouchmove', emptyFunction)
}

return result
}

// 拦截 removeAttribute 来处理 catchMove
el.removeAttribute = function (name: string) {
const oldValue = el.getAttribute(name)

// 处理 catchMove 属性
if (name === 'catchMove' && oldValue) {
el.removeEventListener('catchtouchmove', emptyFunction)
}

return originalRemoveAttribute(name)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

catchMove={false} 时旧的阻止滚动监听不会被移除。

这里只在 removeAttribute 时调用 removeEventListener,但 packages/taro-react/src/props.ts 在 Line 309 只有 value == null 才会走 removeAttributecatchMove={false} 仍然会调用 setAttribute。从 true 切到 false 后,catchtouchmove 空监听会残留,滚动拦截无法关闭。

🔧 建议修复
       el.setAttribute = function (name: string, value: any) {
         const result = originalSetAttribute(name, value)

         // 处理 catchMove 属性
-        if (name === 'catchMove' && value) {
-          el.addEventListener('catchtouchmove', emptyFunction)
+        if (name === 'catchMove') {
+          el.removeEventListener('catchtouchmove', emptyFunction)
+          if (value) {
+            el.addEventListener('catchtouchmove', emptyFunction)
+          }
         }

         return result
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-runtime/src/bom/document.ts` around lines 114 - 135, The
current setAttribute interceptor (function setAttribute) only adds the
emptyFunction listener when catchMove is truthy but never removes it when
catchMove is set to false, causing stale catchtouchmove listeners; update
setAttribute so when name === 'catchMove' and the new value is falsy it calls
el.removeEventListener('catchtouchmove', emptyFunction), and when truthy it
calls el.addEventListener('catchtouchmove', emptyFunction); keep calling
originalSetAttribute(name, value) and preserve the existing removeAttribute
behavior that removes the listener when the attribute is deleted.

@Single-Dancer
Copy link
Copy Markdown
Collaborator

@xzdadada 大佬测试没过,辛苦看一下
tests test:ci: FAIL tests/mini-platform.spec.ts
tests test:ci: ● mini-platform › bytedance › should build bytedance app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants