Skip to content

Conversation

Single-Dancer
Copy link
Collaborator

@Single-Dancer Single-Dancer commented May 6, 2025

这个 PR 做了什么? (简要描述所做更改)
新增 waterflow 组件

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

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

这个 PR 涉及以下平台:

  • 所有小程序
  • 微信小程序
  • 支付宝小程序
  • 百度小程序
  • 字节跳动小程序
  • QQ 轻应用
  • 京东小程序
  • 快应用平台(QuickApp)
  • Web 平台(H5)
  • 移动端(React-Native)
  • 鸿蒙(harmony)

Summary by CodeRabbit

  • 新功能
    • 新增瀑布流(WaterFlow)组件,支持高性能多列布局与虚拟滚动渲染。
    • 提供 FlowSection、FlowItem、FlowItemContainer 等子组件,实现分区管理与单项动态定位。
    • 支持自定义列数、行列间距、滚动阈值及缓存数量,优化大数据量渲染性能。
    • 增加滚动至指定项、滚动阈值触发回调等交互能力。
    • 引入多种辅助 Hook(如 useObservedAttr、useMemoizedFn、useUnmount)及工具函数,提升组件扩展性和稳定性。
    • 实现内部状态管理与事件总线,保障布局计算及滚动状态同步。

@Single-Dancer Single-Dancer requested a review from tutuxxx May 6, 2025 03:01
Copy link

coderabbitai bot commented May 6, 2025

Walkthrough

本次变更在 taro-components-advanced 包中新增了完整的 WaterFlow(水流式瀑布流)布局组件体系,包括核心数据模型(Root、Section、Node)、事件总线(EventBus、StatefulEventBus)、React Hooks(useMemoizedFn、useObservedAttr、useUnmount)、工具函数及相关类型定义。组件支持虚拟滚动、高性能渲染、动态布局和自定义阈值回调,并通过入口文件统一导出相关 API。

Changes

文件/分组 变更摘要
.../src/components/index.ts 新增导出 water-flow 组件模块。
.../src/components/water-flow/index.ts 新增模块,统一导出 WaterFlow 相关组件与类型。
.../src/components/water-flow/flow-item.ts
.../flow-section.ts
.../water-flow.ts
新增 WaterFlow 相关 React 组件(FlowItem、FlowItemContainer、FlowSection、WaterFlow)及其 Hooks。
.../src/components/water-flow/interface.ts 新增 WaterFlow 组件所需的 TypeScript 类型和接口定义。
.../src/components/water-flow/node.ts
.../root.ts
.../section.ts
新增核心数据模型类(Node、Root、Section)及其事件、状态管理。
.../src/components/water-flow/stateful-event-bus.ts 新增事件总线与带状态事件总线(EventBus、StatefulEventBus)实现。
.../src/components/water-flow/use-memoized-fn.ts
.../use-observed-attr.ts
.../use-unmount.ts
新增 React Hooks:useMemoizedFn、useObservedAttr、useUnmount。
.../src/components/water-flow/utils.ts 新增工具函数,包括系统信息获取、矩阵索引转换、渲染范围比较、命令式 Promise、环境判断等。

Sequence Diagram(s)

sequenceDiagram
    participant 用户 as User
    participant WaterFlow as <WaterFlow />
    participant Root as Root
    participant Section as Section
    participant Node as Node
    participant ScrollView as Taro.ScrollView

    用户->>WaterFlow: 渲染组件,传入 children
    WaterFlow->>Root: 初始化 Root,注册参数
    WaterFlow->>Section: 为每个 section 创建 Section 实例
    Section->>Node: 创建并注册 Node
    Root->>Section: 计算可渲染区间
    Section->>Node: 计算节点布局
    用户->>ScrollView: 滚动
    ScrollView->>WaterFlow: onScroll 事件
    WaterFlow->>Root: 更新 scrollOffset、scrollDirection
    Root->>Section: 通知可见区间变更
    Section->>Node: 更新渲染范围
    Node->>WaterFlow: 触发布局、resize 事件
    WaterFlow->>用户: 渲染可见 items
Loading

Poem

兔子新添水流瀑布,
节点分列灵动舞。
虚拟滚动如溪流,
事件总线巧调度。
高性能渲染不怕多,
组件齐全任君用,
代码田野,春风拂兔耳! 🐇🌊

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 15

♻️ Duplicate comments (1)
packages/taro-components-advanced/src/components/water-flow/root.ts (1)

276-315: 再次出现冗余标签 (loopSeciton),且大小写拼写错误
同上,应统一去除标签,避免 lint 报警并提升可读性。

🧰 Tools
🪛 Biome (1.9.4)

[error] 276-276: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

🧹 Nitpick comments (10)
packages/taro-components-advanced/src/components/water-flow/use-unmount.ts (1)

3-9: 依赖项数组可能需要添加回调函数

useUnmount 钩子实现了组件卸载时执行回调的功能,但存在一个潜在问题:依赖数组为空 [],没有包含回调函数 cb。如果 cb 在组件生命周期内发生变化,钩子将继续使用初始版本的回调函数,可能导致闭包陈旧问题。

建议修改实现,将回调函数添加到依赖项中,或者使用 useRef 保存最新的回调引用:

-export function useUnmount(cb: () => void) {
-  useEffect(() => {
-    return () => {
-      cb()
-    }
-  }, [])
-}
+export function useUnmount(cb: () => void) {
+  const cbRef = useRef(cb);
+  
+  useEffect(() => {
+    cbRef.current = cb;
+  });
+  
+  useEffect(() => {
+    return () => {
+      cbRef.current();
+    }
+  }, []);
+}
packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts (1)

10-22: 可简化 useMemoizedFn 实现

useMemoizedFn 钩子实现了函数记忆化,保持引用稳定性同时使用最新的函数版本。但当前实现中 useMemo 的使用方式可能不必要地增加了复杂性。

建议简化实现,直接更新 fnRef.current

export function useMemoizedFn<T extends Noop>(fn: T) {
  const fnRef = useRef<T>(fn)
- fnRef.current = useMemo<T>(() => fn, [fn])
+ fnRef.current = fn
  const memoizedFn = useRef<MemoizedFn<T>>()

  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args)
    }
  }

  return memoizedFn.current as MemoizedFn<T>
}

这种方式会更加简洁,同时保留原有功能。

packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts (1)

29-29: SSR 第三个参数可省略,或改为 undefined 以减少冗余

React 18 之后 useSyncExternalStore 的第三个参数主要用于 SSR。若当前组件不会在服务端渲染,可直接省略;否则应提供符合服务端环境的快照函数,避免误用同一引用。

packages/taro-components-advanced/src/components/water-flow/utils.ts (1)

26-28: isSameRenderRange 使用 JSON.stringify 可能带来性能隐患

当渲染区间数组较大或频繁比较时,JSON.stringify 会产生不必要的字符串分配和 GC 压力。可以改为长度与元素逐一比较,或缓存上一次结果。

-export function isSameRenderRange(a: any[], b: any[]) {
-  return JSON.stringify(a) === JSON.stringify(b)
-}
+export function isSameRenderRange(a: any[], b: any[]) {
+  if (a === b) return true
+  if (a.length !== b.length) return false
+  for (let i = 0; i < a.length; i++) {
+    const [as, ae] = a[i]
+    const [bs, be] = b[i]
+    if (as !== bs || ae !== be) return false
+  }
+  return true
+}
packages/taro-components-advanced/src/components/water-flow/interface.ts (2)

12-18: style 的类型来源不一致,建议统一引用以避免潜在冲突

WaterFlowProps 先在 Omit<ScrollViewProps, ...> 中去掉了 style,随后通过 Pick<BaseProps, 'style'> 重新声明。
然而 BaseProps['style'] 来自 reactCSSProperties,而 ScrollViewProps['style'] 在某些端可能是 Taro 自己的类型别名,二者在部分平台可能并不完全兼容。

可改为直接复用 ScrollViewProps['style'],这样不会丢失 Taro 侧对样式属性的能力倾向:

- Pick<BaseProps, 'style'>
+ { style?: ScrollViewProps['style'] }

这能最大限度保持与小程序端 style 语义的一致性。


50-52: FlowItemContainerProps 强耦合 Node,可通过范型降低循环依赖风险

目前直接把 node 写死为 Node 类型,导致 interface 层与实现层产生循环引用(interface.ts ←→ node.ts)。
考虑改成:

export interface FlowItemContainerProps<T extends Node = Node> extends BaseProps {
  node: T
}

这样既兼容现有实现,又允许后续扩展/mock 更灵活,降低圈复杂度。

packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)

71-80: setStateBatch 中重复调用 getState() 导致不必要的对象创建

在循环里每次都调用 getState() 生成新对象,浪费性能。可先缓存引用:

-    for (const key in s) {
-      if (Object.prototype.hasOwnProperty.call(s, key)) {
-        const rawValue = this.getState()[key]
+    const current = this.state
+    for (const key in s) {
+      if (Object.prototype.hasOwnProperty.call(s, key)) {
+        const rawValue = current[key]

这是一个小优化,但在高频状态批量更新时能减少 GC 压力。

packages/taro-components-advanced/src/components/water-flow/node.ts (1)

101-109: getRectSizeSync 命名与实现不符,可能误导后续维护者
该函数实际返回 Promise,却以 Sync 结尾;此外,此处无需 await 同步版函数。请确认命名与实现一致,或调整调用方式。

packages/taro-components-advanced/src/components/water-flow/section.ts (1)

262-306: 滚动过程频繁 JSON.stringify 可能造成性能瓶颈
getNodeRenderRange 每次都对二维数组做 stringify 比较 O(n²)。在大量节点/高帧率滚动下容易造成卡顿。可改为 version 号或 shallow compare。

packages/taro-components-advanced/src/components/water-flow/root.ts (1)

480-493: 位运算右移用于除以 2 有精度风险
clientHeight >>> 1 会把数字截断为 32 位整数,超出 2^32 的高度将溢出负值。直接使用 / 2 更安全且可读。

-        if (acc >= clientHeight >>> 1) {
+        if (acc >= clientHeight / 2) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 490770c and 927b56b.

📒 Files selected for processing (14)
  • packages/taro-components-advanced/src/components/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-item.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/interface.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/node.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/root.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-unmount.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/utils.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/water-flow.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/taro-components-advanced/src/components/water-flow/section.ts (5)
packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)
  • StatefulEventBus (36-86)
packages/taro-components-advanced/src/components/water-flow/node.ts (1)
  • Node (34-147)
packages/taro-components-advanced/src/components/water-flow/utils.ts (3)
  • createImperativePromise (38-52)
  • getMatrixPosition (17-21)
  • isSameRenderRange (26-28)
packages/taro-components-advanced/src/components/water-flow/root.ts (4)
  • Root (54-500)
  • RootEvents (41-47)
  • scrollBoundaryEnd (356-358)
  • scrollBoundaryStart (349-351)
packages/taro-components-advanced/src/components/water-flow/interface.ts (1)
  • Size (54-57)
packages/taro-components-advanced/src/components/water-flow/root.ts (5)
packages/taro-components-advanced/src/components/water-flow/interface.ts (4)
  • WaterFlowProps (12-33)
  • BaseProps (6-10)
  • ScrollDirection (66-66)
  • Size (54-57)
packages/taro-components-advanced/src/components/water-flow/utils.ts (2)
  • getSysInfo (5-9)
  • isSameRenderRange (26-28)
packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)
  • StatefulEventBus (36-86)
packages/taro-components-advanced/src/components/water-flow/section.ts (1)
  • Section (56-307)
packages/taro-components-advanced/src/components/water-flow/node.ts (1)
  • Node (34-147)
🪛 Biome (1.9.4)
packages/taro-components-advanced/src/components/water-flow/root.ts

[error] 276-276: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)


[error] 340-340: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Rust Binding / stable - x86_64-pc-windows-msvc
  • GitHub Check: Build Rust Binding / stable - aarch64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - x86_64-apple-darwin
🔇 Additional comments (5)
packages/taro-components-advanced/src/components/index.ts (1)

3-3: 组件导出模块已正确更新

新增了对 ./water-flow 模块的导出,使得瀑布流组件可以被外部模块访问和使用。这符合现有的模块导出模式,与其它组件导出保持一致。

packages/taro-components-advanced/src/components/water-flow/index.ts (1)

1-4: 导出结构清晰完整

index 文件正确地导出了所有水流布局组件系统所需的组件和类型。导出结构清晰,包含了组件(FlowItem、FlowSection、WaterFlow)、钩子函数(useFlowItemPositioner)以及必要的类型定义。这种模式使得使用者可以方便地从单一入口引入所需功能。

packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts (1)

19-27: 避免在找不到 state 时直接读取实例属性,降低运行时异常风险

getSnapshot 在无法通过 getState() 取到目标字段时,会直接使用 inst[attr] 作为兜底。然而 StatefulEventBus 的实例本身并不一定暴露同名字段,如果字段不存在将返回 undefined,容易导致 UI 同步失效。建议始终以 getState() 作为唯一可靠来源,或在兜底前做显式存在性校验并给出警告日志。

packages/taro-components-advanced/src/components/water-flow/water-flow.ts (1)

107-117: 缺少对 getScrollViewContextNode 失败的兜底处理

若传入的 selector 未匹配到节点,nodeundefined,再调用 scrollTo 会抛错。建议增加判空:

-    getScrollViewContextNode(`#${root.id}`).then((node: any) => {
-      node.scrollTo({
+    getScrollViewContextNode(`#${root.id}`).then((node: any) => {
+      if (!node) return
+      node.scrollTo({
packages/taro-components-advanced/src/components/water-flow/root.ts (1)

329-341: 阈值判断条件存在逻辑瑕疵
this.lowerThresholdCount !== Infinity 基本恒为 true,若想表达“设置为 Infinity 时关闭下阈值”,应显式比较 Number.POSITIVE_INFINITY 或直接判断 lowerThresholdCount > 0

🧰 Tools
🪛 Biome (1.9.4)

[error] 340-340: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

Copy link

codecov bot commented May 6, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 58.37%. Comparing base (1afdb99) to head (69e110a).
Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main   #17653    +/-   ##
========================================
  Coverage   58.37%   58.37%            
========================================
  Files         469      469            
  Lines       27230    27230            
  Branches     5984     5777   -207     
========================================
  Hits        15895    15895            
+ Misses       9965     9839   -126     
- Partials     1370     1496   +126     
Flag Coverage Δ
taro-cli 72.37% <ø> (ø)
taro-runtime 59.87% <ø> (ø)
taro-web 52.75% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

see 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Single-Dancer Single-Dancer added this to the 4.0.13 milestone May 6, 2025
Copy link

@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-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)

7-14: 🛠️ Refactor suggestion

发布事件时应支持通配符事件监听

当前 pub 方法只向特定事件的订阅者发布事件,没有处理通配符订阅者的逻辑。如果在代码其他部分有使用通配符(如 Symbol.for('broadcast'))来监听所有事件的需求,这些监听器将无法收到事件通知。

建议修改实现以支持通配符事件:

  pub<P = undefined>(ev: T, payload?: P) {
    const set = this.eventsMap.get(ev)
    if (set) {
      for (const cb of set) {
        cb(ev, payload)
      }
    }
+   // 支持通配符事件监听
+   const broadcast = Symbol.for('broadcast')
+   const broadcastSet = this.eventsMap.get(broadcast as unknown as T)
+   if (broadcastSet) {
+     for (const cb of broadcastSet) {
+       cb(ev, payload)
+     }
+   }
  }
packages/taro-components-advanced/src/components/water-flow/node.ts (1)

83-95: ⚠️ Potential issue

节点测量过程缺少超时保护机制

当前 NodeEvents.Resize 事件处理中直接 await this.measure(),根据已知的学习记录,如果节点不存在,getRectSizeSync 可能会一直处于 pending 状态,导致整个布局过程被阻塞。

建议添加超时机制:

  this.sub(NodeEvents.Resize, async () => {
    const { width, height } = this.getState()
-   const newSize = await this.measure()
+   // 添加超时保护机制
+   const timeoutPromise = new Promise<{ width: number; height: number }>((_, reject) => {
+     setTimeout(() => reject(new Error('Measure timeout')), 300) // 300ms 超时
+   })
+   
+   let newSize: { width: number; height: number }
+   try {
+     newSize = await Promise.race([this.measure(), timeoutPromise])
+   } catch (err) {
+     console.warn(`测量节点 ${this.id} 失败:`, err)
+     return
+   }
    if (newSize.height === height && newSize.width === width) {
      return
    }
    this.updateBehindNodesPosition()
    this.section.pub(SectionEvents.Resize, {
      node: this,
      newSize,
      originalSize: { width, height },
    })
  })
packages/taro-components-advanced/src/components/water-flow/root.ts (1)

213-253: 🛠️ Refactor suggestion

不必要的 label 标签使用

setUpperThresholdScrollTop 方法中使用了 loopSecitonloopItem 标签来控制嵌套循环,使代码结构复杂化且被静态分析工具标记为问题。

建议重构为更清晰的控制流:

- loopSeciton: for (let i = 0; i < sectionSize; i++) {
+ for (let i = 0; i < sectionSize; i++) {
  // ... 其他代码
-     loopItem: for (let j = 0; j < columnSize; j++) {
+     for (let j = 0; j < columnSize; j++) {
        colTracker.accCount += 1
        colTracker.accHeight +=
          column[j].getState().height + (j === 0 ? 0 : section.rowGap)
        if (colTracker.accCount >= this.upperThresholdCount) {
-         break loopItem
+         break
        }
      }
    }
    for (const [, colTracker] of sectionTracker) {
      if (colTracker.accCount >= this.upperThresholdCount) {
        this.upperThresholdScrollTop = colTracker.accHeight
-       break loopSeciton
+       return this.upperThresholdScrollTop
      }
    }
  }

这样的重构提高了代码可读性,同时消除了静态分析工具的警告。

🧹 Nitpick comments (6)
packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)

69-79: 批量更新状态时未处理深层对象比较

当前的 setStateBatch 方法使用 Object.is 进行浅层比较,这对于复杂的嵌套对象/数组状态可能导致更新被忽略。

考虑添加深度比较选项或文档说明这一限制:

  setStateBatch(s: Partial<S>) {
    for (const key in s) {
      if (Object.prototype.hasOwnProperty.call(s, key)) {
        const rawValue = this.getState()[key]
        const newValue = s[key]
-       if (!Object.is(rawValue, newValue)) {
+       // 简单类型使用 Object.is,对象类型可考虑添加深度比较
+       if (!Object.is(rawValue, newValue) && 
+           (typeof rawValue !== 'object' || 
+            JSON.stringify(rawValue) !== JSON.stringify(newValue))) {
          this.setStateIn(key, newValue)
        }
      }
    }
  }

或者在类注释中说明此限制,提醒开发者在使用复杂对象时需手动触发更新。

packages/taro-components-advanced/src/components/water-flow/section.ts (2)

121-131: Resize 事件处理使用了防抖但未指定延迟时间

当前对 SectionEvents.Resize 事件使用了 debounce 包装,这对于频繁触发的调整大小事件是很好的性能优化。但是,没有显式指定防抖延迟时间,可能导致在不同设备上的行为不一致。

建议明确指定防抖延迟时间:

  this.sub<{ node: Node, newSize: Size, originalSize: Size }>(
    SectionEvents.Resize,
-   debounce(() => {
+   debounce(() => {
      this.setStateIn('height', this.maxColumnHeight)
      this.updateBehindSectionsPosition()
      if (this.isInRange) {
        this.setStateIn('renderRange', this.getNodeRenderRange())
      }
      this.root.pub(RootEvents.Resize)
-   })
+   }, 16) // 约定 16ms 延迟(一帧的时间)
  )

261-305: 渲染范围计算逻辑复杂,考虑拆分

getNodeRenderRange 方法包含多层嵌套循环和复杂的条件判断,降低了代码可读性和可维护性。

建议将该方法拆分为更小的辅助函数,如:

  • calculateVisibleNodes(column, columnIndex) - 计算每列中可见的节点
  • applyOverscan(range, direction) - 应用预加载范围

这样可以提高代码的可读性和可测试性,同时保持功能不变。

packages/taro-components-advanced/src/components/water-flow/root.ts (3)

1-1: 使用 eslint-disable 忽略标签规则

文件开头使用 eslint-disable no-labels 注释禁用了标签检查规则。尽管在某些情况下使用标签可以简化复杂逻辑的控制流,但它也可能降低代码可读性和可维护性。

考虑重构使用标签的代码部分,改用更清晰的控制流结构(如提前返回、辅助函数等)。


462-494: 使用位操作符降低代码可读性

calcCacheSection 方法中,使用了位移操作符 >>> 进行除 2 操作(第 475 和 487 行)。虽然这是一个微小的优化,但降低了代码可读性。

建议使用更清晰的表达式:

- if (acc >= clientHeight >>> 1) {
+ if (acc >= clientHeight / 2) {

这样对大多数开发者来说更容易理解,同时现代 JavaScript 引擎通常会优化这类简单的数学运算。


169-194: 递归渲染可能导致调用栈溢出

renderInitialLayout 方法使用递归方式渲染分组。当分组数量非常大时,可能导致调用栈溢出。

考虑使用迭代方法或分批处理:

renderInitialLayout(i = 0) {
  const processNextSection = () => {
    Taro.nextTick(() => {
      const sectionSize = this.sections.length;
      
      if (i >= sectionSize || i < 0) {
        this.pub(RootEvents.InitialRenderCompleted, null);
        return;
      }
      
      const section = this.sections[i];
      section.layoutedSignal.promise.then(() => {
        this.setStateIn('renderRange', [
          0,
          i + 1 > sectionSize ? sectionSize - 1 : i + 1,
        ]);
        
        // 容器可视区域已经填满了,没必要再继续
        if (section.getState().scrollTop > this.getState().containerSize.height) {
          this.pub(RootEvents.InitialRenderCompleted, section);
          return;
        }
        
        i++;
        processNextSection();
      });
    });
  };
  
  processNextSection();
}

这样虽然功能相同,但避免了真正的递归调用导致的潜在栈溢出风险。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 927b56b and f0420ea.

📒 Files selected for processing (14)
  • packages/taro-components-advanced/src/components/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-item.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/interface.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/node.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/root.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-unmount.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/utils.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/water-flow.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/taro-components-advanced/src/components/index.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/taro-components-advanced/src/components/water-flow/use-unmount.ts
  • packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts
  • packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts
  • packages/taro-components-advanced/src/components/water-flow/index.ts
  • packages/taro-components-advanced/src/components/water-flow/interface.ts
  • packages/taro-components-advanced/src/components/water-flow/flow-section.ts
  • packages/taro-components-advanced/src/components/water-flow/flow-item.ts
  • packages/taro-components-advanced/src/components/water-flow/water-flow.ts
  • packages/taro-components-advanced/src/components/water-flow/utils.ts
🧰 Additional context used
🧠 Learnings (1)
packages/taro-components-advanced/src/components/water-flow/node.ts (3)
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点,会一直处于 pending 状态而不是抛出错误。在这种情况下,应该使用 Promise.race 添加超时机制,而不是仅依赖 try/catch 来处理错误。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点并且重试次数用尽,会一直处于 pending 状态而不是抛出错误,因为它在 retryTimes <= 0 时直接返回而不 resolve 或 reject Promise。应该使用 Promise.race 添加超时机制来解决这个问题。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 getRectSizeSync 函数中,如果找不到指定的节点,函数会一直处于 pending 状态而不是抛出错误。在这种情况下,使用 try/catch 处理不够充分,需要添加超时处理如 Promise.race 来避免无限等待。
🧬 Code Graph Analysis (1)
packages/taro-components-advanced/src/components/water-flow/section.ts (5)
packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)
  • StatefulEventBus (34-84)
packages/taro-components-advanced/src/components/water-flow/node.ts (1)
  • Node (34-148)
packages/taro-components-advanced/src/components/water-flow/utils.ts (3)
  • createImperativePromise (38-52)
  • getMatrixPosition (17-21)
  • isSameRenderRange (26-28)
packages/taro-components-advanced/src/components/water-flow/root.ts (4)
  • Root (54-495)
  • RootEvents (41-47)
  • scrollBoundaryEnd (351-353)
  • scrollBoundaryStart (344-346)
packages/taro-components-advanced/src/components/water-flow/interface.ts (1)
  • Size (55-58)
🪛 Biome (1.9.4)
packages/taro-components-advanced/src/components/water-flow/root.ts

[error] 273-273: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)


[error] 344-344: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Build Rust Binding / stable - x86_64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - aarch64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - x86_64-pc-windows-msvc
  • GitHub Check: Build Rust Binding / stable - x86_64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - x86_64-pc-windows-msvc
  • GitHub Check: Build Rust Binding / stable - aarch64-apple-darwin
🔇 Additional comments (4)
packages/taro-components-advanced/src/components/water-flow/node.ts (2)

114-134: 更新节点位置时正确考虑了行间距

当前实现在 updateBehindNodesPosition 方法中正确地考虑了 rowGap,避免了滚动过程中图片等异步内容拉伸后,后续节点与前节点重叠的问题。这是一个很好的改进。


140-147: 计算节点可见性的逻辑清晰且高效

isInRange 计算器通过比较节点的滚动位置和高度与根容器的滚动边界,高效地确定节点是否在可视区域内。这种实现避免了每次滚动时对所有节点进行完整的边界计算。

packages/taro-components-advanced/src/components/water-flow/section.ts (2)

138-144: 最大列高计算正确考虑了行间距

maxColumnHeight 计算中,通过添加 (column.length - 1) * this.rowGap 正确地将行间距计入总高度,这确保了布局精确度。


215-238: 列内节点定位正确应用了行间距

updateColumnNode 方法中,通过在计算 scrollToptop 值时加入 this.rowGap,确保了节点之间有适当的间距,防止在滚动和内容调整时出现重叠。

Comment on lines +109 to +118
this.setupSubscriptions()
getRectSizeSync(`#${id}`, 100).then(
({ width = windowWidth, height = windowHeight }) => {
this.setStateIn('containerSize', {
width,
height,
})
}
)
this.renderInitialLayout()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

缺少异步错误处理机制

在构造函数中,对 getRectSizeSync 的调用没有错误处理机制。如果获取容器尺寸失败,可能导致组件初始化不完整。

建议添加错误处理:

- getRectSizeSync(`#${id}`, 100).then(
+ getRectSizeSync(`#${id}`, 100).then(
    ({ width = windowWidth, height = windowHeight }) => {
      this.setStateIn('containerSize', {
        width,
        height,
      })
    }
- )
+ ).catch(err => {
+   console.warn(`获取容器 #${id} 尺寸失败:`, err)
+   // 使用默认尺寸
+   this.setStateIn('containerSize', {
+     width: windowWidth,
+     height: windowHeight,
+   })
+ })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.setupSubscriptions()
getRectSizeSync(`#${id}`, 100).then(
({ width = windowWidth, height = windowHeight }) => {
this.setStateIn('containerSize', {
width,
height,
})
}
)
this.renderInitialLayout()
this.setupSubscriptions()
getRectSizeSync(`#${id}`, 100).then(
({ width = windowWidth, height = windowHeight }) => {
this.setStateIn('containerSize', {
width,
height,
})
}
).catch(err => {
console.warn(`获取容器 #${id} 尺寸失败:`, err)
// 使用默认尺寸
this.setStateIn('containerSize', {
width: windowWidth,
height: windowHeight,
})
})
this.renderInitialLayout()
🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/root.ts around lines 109 to 118, the call to getRectSizeSync lacks error handling for the asynchronous operation. To fix this, add a catch block to the promise chain to handle any errors that occur during the size retrieval, ensuring the component can handle failures gracefully and maintain proper initialization.

Comment on lines +273 to +313
loopSeciton: for (let i = sectionSize - 1; i > 0; i--) {
const section = this.sections[i]
const sectionTracker = tracker[i]
const columnMap = section.columnMap
// 扫描当前分组的每一列
for (let col = 0; col < columnMap.length; col++) {
const column = columnMap[col]
const columnSize = column.length
if (!sectionTracker.has(col)) {
if (i === sectionSize - 1) {
sectionTracker.set(col, { accCount: 0, scrollTop: 0 })
} else {
const belowSectionTracker = tracker[i + 1]
sectionTracker.set(col, {
accCount: Math.max(
...[...belowSectionTracker.values()].map(
(nodeTracker) => nodeTracker.accCount
)
),
scrollTop: 0,
})
}
}
const colTracker = sectionTracker.get(col)!
// 从当前列的最后一行开始往前扫描
loopItem: for (let j = columnSize - 1; j >= 0; j--) {
colTracker.accCount += 1
colTracker.scrollTop = column[j].getState().scrollTop
if (colTracker.accCount >= this.lowerThresholdCount) {
break loopItem
}
}
}

for (const [, colTracker] of sectionTracker) {
if (colTracker.accCount >= this.lowerThresholdCount) {
this.lowerThresholdScrollTop = colTracker.scrollTop
break loopSeciton
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

不必要的 label 标签使用

setUpperThresholdScrollTop 类似,setLowerThresholdScrollTop 方法也使用了 loopSecitonloopItem 标签控制流,增加了复杂度且触发了静态分析警告。

建议采用相同的重构策略,使用提前返回代替标签:

- loopSeciton: for (let i = sectionSize - 1; i > 0; i--) {
+ for (let i = sectionSize - 1; i > 0; i--) {
  // ... 其他代码
-     loopItem: for (let j = columnSize - 1; j >= 0; j--) {
+     for (let j = columnSize - 1; j >= 0; j--) {
        colTracker.accCount += 1
        colTracker.scrollTop = column[j].getState().scrollTop
        if (colTracker.accCount >= this.lowerThresholdCount) {
-         break loopItem
+         break
        }
      }
    }

    for (const [, colTracker] of sectionTracker) {
      if (colTracker.accCount >= this.lowerThresholdCount) {
        this.lowerThresholdScrollTop = colTracker.scrollTop
-       break loopSeciton
+       return this.lowerThresholdScrollTop
      }
    }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loopSeciton: for (let i = sectionSize - 1; i > 0; i--) {
const section = this.sections[i]
const sectionTracker = tracker[i]
const columnMap = section.columnMap
// 扫描当前分组的每一列
for (let col = 0; col < columnMap.length; col++) {
const column = columnMap[col]
const columnSize = column.length
if (!sectionTracker.has(col)) {
if (i === sectionSize - 1) {
sectionTracker.set(col, { accCount: 0, scrollTop: 0 })
} else {
const belowSectionTracker = tracker[i + 1]
sectionTracker.set(col, {
accCount: Math.max(
...[...belowSectionTracker.values()].map(
(nodeTracker) => nodeTracker.accCount
)
),
scrollTop: 0,
})
}
}
const colTracker = sectionTracker.get(col)!
// 从当前列的最后一行开始往前扫描
loopItem: for (let j = columnSize - 1; j >= 0; j--) {
colTracker.accCount += 1
colTracker.scrollTop = column[j].getState().scrollTop
if (colTracker.accCount >= this.lowerThresholdCount) {
break loopItem
}
}
}
for (const [, colTracker] of sectionTracker) {
if (colTracker.accCount >= this.lowerThresholdCount) {
this.lowerThresholdScrollTop = colTracker.scrollTop
break loopSeciton
}
}
}
for (let i = sectionSize - 1; i > 0; i--) {
const section = this.sections[i]
const sectionTracker = tracker[i]
const columnMap = section.columnMap
// 扫描当前分组的每一列
for (let col = 0; col < columnMap.length; col++) {
const column = columnMap[col]
const columnSize = column.length
if (!sectionTracker.has(col)) {
if (i === sectionSize - 1) {
sectionTracker.set(col, { accCount: 0, scrollTop: 0 })
} else {
const belowSectionTracker = tracker[i + 1]
sectionTracker.set(col, {
accCount: Math.max(
...[...belowSectionTracker.values()].map(
(nodeTracker) => nodeTracker.accCount
)
),
scrollTop: 0,
})
}
}
const colTracker = sectionTracker.get(col)!
// 从当前列的最后一行开始往前扫描
for (let j = columnSize - 1; j >= 0; j--) {
colTracker.accCount += 1
colTracker.scrollTop = column[j].getState().scrollTop
if (colTracker.accCount >= this.lowerThresholdCount) {
break
}
}
}
for (const [, colTracker] of sectionTracker) {
if (colTracker.accCount >= this.lowerThresholdCount) {
this.lowerThresholdScrollTop = colTracker.scrollTop
return this.lowerThresholdScrollTop
}
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 273-273: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/root.ts between lines 273 and 313, the method setLowerThresholdScrollTop uses labeled loops 'loopSeciton' and 'loopItem' to control flow, which adds unnecessary complexity and triggers static analysis warnings. Refactor the code by removing these labels and replacing the labeled breaks with early returns or flag variables to exit loops cleanly, simplifying the control flow and improving readability.

Copy link

@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: 4

♻️ Duplicate comments (3)
packages/taro-components-advanced/src/components/water-flow/root.ts (3)

109-118: ⚠️ Potential issue

缺少异步错误处理机制

在构造函数中,对 getRectSizeSync 的调用没有错误处理机制。如果获取容器尺寸失败,可能导致组件初始化不完整。

建议添加错误处理:

- getRectSizeSync(`#${id}`, 100).then(
+ getRectSizeSync(`#${id}`, 100).then(
    ({ width = windowWidth, height = windowHeight }) => {
      this.setStateIn('containerSize', {
        width,
        height,
      })
    }
- )
+ ).catch(err => {
+   console.warn(`获取容器 #${id} 尺寸失败:`, err)
+   // 使用默认尺寸
+   this.setStateIn('containerSize', {
+     width: windowWidth,
+     height: windowHeight,
+   })
+ })

213-253: 🛠️ Refactor suggestion

不必要的标签使用增加代码复杂度

使用标签 loopSecitonloopItem 控制流程使代码复杂化,并触发了静态分析警告。可通过提前返回或使用函数封装来消除标签使用。

建议采用以下重构方案:

- loopSeciton: for (let i = 0; i < sectionSize; i++) {
+ for (let i = 0; i < sectionSize; i++) {
  const section = this.sections[i]
  const sectionTracker = tracker[i]
  const columnMap = section.columnMap
  // 扫描当前分组的每一列
  for (let col = 0; col < columnMap.length; col++) {
    // ...现有代码...
  }
  for (const [, colTracker] of sectionTracker) {
    if (colTracker.accCount >= this.upperThresholdCount) {
      this.upperThresholdScrollTop = colTracker.accHeight
-     break loopSeciton
+     return this.upperThresholdScrollTop
    }
  }
}

+ return this.upperThresholdScrollTop

或者考虑提取为独立函数,使逻辑更清晰。


273-313: 🛠️ Refactor suggestion

setLowerThresholdScrollTop方法也使用了不必要的标签

setUpperThresholdScrollTop 类似,setLowerThresholdScrollTop 方法也使用了 loopSecitonloopItem 标签控制流,增加了复杂度且触发了静态分析警告。

建议采用相同的重构策略,使用提前返回代替标签:

- loopSeciton: for (let i = sectionSize - 1; i > 0; i--) {
+ for (let i = sectionSize - 1; i > 0; i--) {
  // ... 其他代码
-     loopItem: for (let j = columnSize - 1; j >= 0; j--) {
+     for (let j = columnSize - 1; j >= 0; j--) {
      colTracker.accCount += 1
      colTracker.scrollTop = column[j].getState().scrollTop
      if (colTracker.accCount >= this.lowerThresholdCount) {
-       break loopItem
+       break
      }
    }
  }

  for (const [, colTracker] of sectionTracker) {
    if (colTracker.accCount >= this.lowerThresholdCount) {
      this.lowerThresholdScrollTop = colTracker.scrollTop
-     break loopSeciton
+     return this.lowerThresholdScrollTop
    }
  }
}

+ return this.lowerThresholdScrollTop
🧰 Tools
🪛 Biome (1.9.4)

[error] 273-273: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

🧹 Nitpick comments (3)
packages/taro-components-advanced/src/components/water-flow/node.ts (1)

102-102: measure方法中应添加重试次数参数

getRectSizeSync 第三个参数是重试次数,当前值 3 是硬编码的。如果节点加载较慢,可能需要更多的重试次数。

建议将重试次数参数化,以便更灵活地根据不同场景调整:

- public async measure() {
+ public async measure(retryTimes = 3) {
-   const { height, width } = await getRectSizeSync(`#${this.id}`, 100, 3)
+   const { height, width } = await getRectSizeSync(`#${this.id}`, 100, retryTimes)
    this.setStateBatch({
      width,
      height,
      layouted: true,
    })
    return { width, height }
  }
packages/taro-components-advanced/src/components/water-flow/section.ts (1)

205-205: 分组间距处理存在不一致

当更新后续分组位置时,使用了 this.rowGap 作为分组间距,但实际上应该使用根容器统一的分组间距,而不是当前分组的行间距。

建议考虑添加一个专门的 sectionGap 属性来控制分组之间的间距,或确保使用的 rowGap 是合适的:

nextSection.setStateIn(
  'scrollTop',
- currentSection.getState().scrollTop + currentSection.getState().height + this.rowGap
+ currentSection.getState().scrollTop + currentSection.getState().height + this.root.sectionGap
)

如果确实需要复用 rowGap 作为分组间的间距,请在代码注释中明确说明这种设计意图。

packages/taro-components-advanced/src/components/water-flow/root.ts (1)

213-213: 标签名称中存在拼写错误

标签名 loopSeciton 拼写错误,应为 loopSection。虽然功能上没有问题,但代码可读性会受影响。

更正拼写错误:

- loopSeciton: for (let i = 0; i < sectionSize; i++) {
+ loopSection: for (let i = 0; i < sectionSize; i++) {

不过,正如静态分析工具所提示的,更好的解决方案是重构代码以移除不必要的标签用法。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0420ea and 69e110a.

📒 Files selected for processing (14)
  • packages/taro-components-advanced/src/components/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-item.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/flow-section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/index.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/interface.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/node.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/root.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/section.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/use-unmount.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/utils.ts (1 hunks)
  • packages/taro-components-advanced/src/components/water-flow/water-flow.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/taro-components-advanced/src/components/index.ts
  • packages/taro-components-advanced/src/components/water-flow/use-unmount.ts
  • packages/taro-components-advanced/src/components/water-flow/index.ts
  • packages/taro-components-advanced/src/components/water-flow/use-observed-attr.ts
  • packages/taro-components-advanced/src/components/water-flow/flow-section.ts
  • packages/taro-components-advanced/src/components/water-flow/water-flow.ts
  • packages/taro-components-advanced/src/components/water-flow/interface.ts
  • packages/taro-components-advanced/src/components/water-flow/flow-item.ts
  • packages/taro-components-advanced/src/components/water-flow/utils.ts
  • packages/taro-components-advanced/src/components/water-flow/use-memoized-fn.ts
  • packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts
🧰 Additional context used
🧠 Learnings (2)
packages/taro-components-advanced/src/components/water-flow/node.ts (3)
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点,会一直处于 pending 状态而不是抛出错误。在这种情况下,应该使用 Promise.race 添加超时机制,而不是仅依赖 try/catch 来处理错误。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点并且重试次数用尽,会一直处于 pending 状态而不是抛出错误,因为它在 retryTimes <= 0 时直接返回而不 resolve 或 reject Promise。应该使用 Promise.race 添加超时机制来解决这个问题。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 getRectSizeSync 函数中,如果找不到指定的节点,函数会一直处于 pending 状态而不是抛出错误。在这种情况下,使用 try/catch 处理不够充分,需要添加超时处理如 Promise.race 来避免无限等待。
packages/taro-components-advanced/src/components/water-flow/root.ts (3)
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点,会一直处于 pending 状态而不是抛出错误。在这种情况下,应该使用 Promise.race 添加超时机制,而不是仅依赖 try/catch 来处理错误。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 water-flow 组件中,getRectSizeSync 函数如果找不到指定的节点并且重试次数用尽,会一直处于 pending 状态而不是抛出错误,因为它在 retryTimes <= 0 时直接返回而不 resolve 或 reject Promise。应该使用 Promise.race 添加超时机制来解决这个问题。
Learnt from: Single-Dancer
PR: NervJS/taro#17653
File: packages/taro-components-advanced/src/components/water-flow/node.ts:83-95
Timestamp: 2025-05-06T06:55:44.054Z
Learning: 在 Taro 的 getRectSizeSync 函数中,如果找不到指定的节点,函数会一直处于 pending 状态而不是抛出错误。在这种情况下,使用 try/catch 处理不够充分,需要添加超时处理如 Promise.race 来避免无限等待。
🧬 Code Graph Analysis (1)
packages/taro-components-advanced/src/components/water-flow/root.ts (5)
packages/taro-components-advanced/src/components/water-flow/interface.ts (4)
  • WaterFlowProps (13-34)
  • BaseProps (7-11)
  • ScrollDirection (67-67)
  • Size (55-58)
packages/taro-components-advanced/src/components/water-flow/utils.ts (2)
  • getSysInfo (5-9)
  • isSameRenderRange (26-28)
packages/taro-components-advanced/src/components/water-flow/stateful-event-bus.ts (1)
  • StatefulEventBus (34-84)
packages/taro-components-advanced/src/components/water-flow/section.ts (1)
  • Section (55-306)
packages/taro-components-advanced/src/components/water-flow/node.ts (1)
  • Node (34-148)
🪛 Biome (1.9.4)
packages/taro-components-advanced/src/components/water-flow/root.ts

[error] 273-273: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)


[error] 344-344: Unnecessary label.

Safe fix: Remove the unnecessary label.
You can achieve the same result without the label.

(lint/complexity/noUselessLabel)

⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Build Rust Binding / stable - x86_64-pc-windows-msvc
  • GitHub Check: Build Rust Binding / stable - aarch64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - aarch64-apple-darwin
  • GitHub Check: Build Rust Binding / stable - x86_64-pc-windows-msvc
  • GitHub Check: Build Rust Binding / stable - x86_64-apple-darwin
  • GitHub Check: Build Rust WASM / stable - wasm32-wasi

Comment on lines +83 to +95
this.sub(NodeEvents.Resize, async () => {
const { width, height } = this.getState()
const newSize = await this.measure()
if (newSize.height === height && newSize.width === width) {
return
}
this.updateBehindNodesPosition()
this.section.pub(SectionEvents.Resize, {
node: this,
newSize,
originalSize: { width, height },
})
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

需要添加超时处理机制以防 measure 挂起

根据您之前提供的反馈,当节点不存在时,getRectSizeSync 会一直处于 pending 状态而不是抛出错误。Resize 事件处理程序中直接 await measure() 可能导致整个流程被阻塞。

建议使用 Promise.race 添加超时机制:

this.sub(NodeEvents.Resize, async () => {
  const { width, height } = this.getState()
- const newSize = await this.measure()
+ // 添加超时保护
+ const timeoutPromise = new Promise<{ width: number; height: number }>((_, reject) => {
+   setTimeout(() => reject(new Error('Measure timeout')), 300) // 300ms 超时
+ })
+ 
+ let newSize: { width: number; height: number }
+ try {
+   newSize = await Promise.race([this.measure(), timeoutPromise])
+ } catch (err) {
+   console.warn(`测量节点 ${this.id} 失败:`, err)
+   return
+ }
  if (newSize.height === height && newSize.width === width) {
    return
  }
  this.updateBehindNodesPosition()
  this.section.pub(SectionEvents.Resize, {
    node: this,
    newSize,
    originalSize: { width, height },
  })
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.sub(NodeEvents.Resize, async () => {
const { width, height } = this.getState()
const newSize = await this.measure()
if (newSize.height === height && newSize.width === width) {
return
}
this.updateBehindNodesPosition()
this.section.pub(SectionEvents.Resize, {
node: this,
newSize,
originalSize: { width, height },
})
})
this.sub(NodeEvents.Resize, async () => {
const { width, height } = this.getState()
// 添加超时保护
const timeoutPromise = new Promise<{ width: number; height: number }>((_, reject) => {
setTimeout(() => reject(new Error('Measure timeout')), 300) // 300ms 超时
})
let newSize: { width: number; height: number }
try {
newSize = await Promise.race([this.measure(), timeoutPromise])
} catch (err) {
console.warn(`测量节点 ${this.id} 失败:`, err)
return
}
if (newSize.height === height && newSize.width === width) {
return
}
this.updateBehindNodesPosition()
this.section.pub(SectionEvents.Resize, {
node: this,
newSize,
originalSize: { width, height },
})
})
🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/node.ts around lines 83 to 95, the async Resize event handler awaits measure() directly, which can cause blocking if measure hangs due to getRectSizeSync never resolving. To fix this, wrap the measure() call with Promise.race against a timeout promise to ensure the await does not hang indefinitely. Implement a timeout mechanism that rejects or resolves after a set duration, and handle the timeout case appropriately to prevent blocking the Resize event processing.

Comment on lines +252 to +256
private calcScrollTop() {
return this.root.sections
.slice(0, this.order)
.reduce((acc, section) => acc + section.maxColumnHeight, 0)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

calcScrollTop方法计算有误

当前的 calcScrollTop 方法只累加了之前所有分组的高度,但没有考虑分组之间的间距。

需要在分组高度累加时也添加间距:

private calcScrollTop() {
  return this.root.sections
    .slice(0, this.order)
-   .reduce((acc, section) => acc + section.maxColumnHeight, 0)
+   .reduce((acc, section, index, array) => {
+     // 添加分组高度
+     const sectionHeight = section.maxColumnHeight;
+     // 如果不是最后一个分组,还需要加上间距
+     const gap = index < array.length - 1 ? section.rowGap : 0;
+     return acc + sectionHeight + gap;
+   }, 0)
}
🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/section.ts around lines 252 to 256, the calcScrollTop method incorrectly calculates the scroll top by summing only the maxColumnHeight of previous sections without including the spacing between sections. To fix this, modify the calculation to add the spacing value for each section in addition to its height during the reduce operation, ensuring the total scroll top accounts for both heights and gaps between sections.

@@ -0,0 +1,495 @@
/* eslint-disable no-labels */
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

不必要的ESLint禁用注释

文件顶部使用了 /* eslint-disable no-labels */ 注释,这通常是临时解决方案。标签的使用会增加代码复杂度,应考虑重构代码而不是禁用 lint 规则。

建议删除此注释,并重构使用标签的代码部分,如 setUpperThresholdScrollTopsetLowerThresholdScrollTop 方法。

🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/root.ts at line 1, there is an unnecessary ESLint disable comment for no-labels. Remove the /* eslint-disable no-labels */ comment and refactor the code in the setUpperThresholdScrollTop and setLowerThresholdScrollTop methods to eliminate the use of labels, restructuring the logic to avoid labeled statements and improve code clarity.

Comment on lines +169 to +194
renderInitialLayout(i = 0) {
Taro.nextTick(() => {
const sectionSize = this.sections.length

if (i >= sectionSize || i < 0) {
this.pub(RootEvents.InitialRenderCompleted, null)
return
}
const section = this.sections[i]
section.layoutedSignal.promise.then(() => {
this.setStateIn('renderRange', [
0,
i + 1 > sectionSize ? sectionSize - 1 : i + 1,
])
// 容器可视区域已经填满了,没必要再继续
if (
section.getState().scrollTop > this.getState().containerSize.height
) {
this.pub(RootEvents.InitialRenderCompleted, section)
return
}

this.renderInitialLayout(i + 1)
})
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

renderInitialLayout方法缺少错误处理

如果 layoutedSignal.promise 被拒绝,当前实现没有任何错误处理机制,可能导致初始渲染中断且没有提示。

建议添加 catch 块来处理可能的错误:

section.layoutedSignal.promise.then(() => {
  this.setStateIn('renderRange', [
    0,
    i + 1 > sectionSize ? sectionSize - 1 : i + 1,
  ])
  // 容器可视区域已经填满了,没必要再继续
  if (
    section.getState().scrollTop > this.getState().containerSize.height
  ) {
    this.pub(RootEvents.InitialRenderCompleted, section)
    return
  }

  this.renderInitialLayout(i + 1)
+ }).catch(err => {
+   console.warn(`分组 ${section.id} 布局计算失败:`, err)
+   // 即使某个分组失败也继续后续渲染
+   this.renderInitialLayout(i + 1)
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
renderInitialLayout(i = 0) {
Taro.nextTick(() => {
const sectionSize = this.sections.length
if (i >= sectionSize || i < 0) {
this.pub(RootEvents.InitialRenderCompleted, null)
return
}
const section = this.sections[i]
section.layoutedSignal.promise.then(() => {
this.setStateIn('renderRange', [
0,
i + 1 > sectionSize ? sectionSize - 1 : i + 1,
])
// 容器可视区域已经填满了,没必要再继续
if (
section.getState().scrollTop > this.getState().containerSize.height
) {
this.pub(RootEvents.InitialRenderCompleted, section)
return
}
this.renderInitialLayout(i + 1)
})
})
}
renderInitialLayout(i = 0) {
Taro.nextTick(() => {
const sectionSize = this.sections.length
if (i >= sectionSize || i < 0) {
this.pub(RootEvents.InitialRenderCompleted, null)
return
}
const section = this.sections[i]
section.layoutedSignal.promise.then(() => {
this.setStateIn('renderRange', [
0,
i + 1 > sectionSize ? sectionSize - 1 : i + 1,
])
// 容器可视区域已经填满了,没必要再继续
if (
section.getState().scrollTop > this.getState().containerSize.height
) {
this.pub(RootEvents.InitialRenderCompleted, section)
return
}
this.renderInitialLayout(i + 1)
}).catch(err => {
console.warn(`分组 ${section.id} 布局计算失败:`, err)
// 即使某个分组失败也继续后续渲染
this.renderInitialLayout(i + 1)
})
})
}
🤖 Prompt for AI Agents (early access)
In packages/taro-components-advanced/src/components/water-flow/root.ts around lines 169 to 194, the renderInitialLayout method lacks error handling for the layoutedSignal.promise. To fix this, add a catch block after the then call on layoutedSignal.promise to handle any rejected promises. In the catch block, log or handle the error appropriately to prevent the initial rendering process from silently failing.

@Single-Dancer Single-Dancer merged commit 6a5c39e into main May 6, 2025
39 checks passed
@Single-Dancer Single-Dancer deleted the feat/waterflow branch May 6, 2025 08:33
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