ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flet use_effect 钩子完全指南:在声明式组件中管理副作用与生命周期

Flet use_effect 钩子完全指南:在声明式组件中管理副作用与生命周期 Flet use_effect 钩子完全指南在声明式组件中管理副作用与生命周期【免费下载链接】fletBuild realtime web, mobile and desktop apps in Python only. No frontend experience required.项目地址: https://gitcode.com/gh_mirrors/fl/fletuse_effect是 Flet 声明式 UI 中用于在函数式组件内执行副作用的内置 Hook副作用指状态渲染之外的一切操作如定时器、数据订阅、页面事件、异步任务等。本文围绕flet.use_effect的完整 APIsetup / dependencies / cleanup 三个参数、依赖变更触发规则、on_mounted/on_unmounted/on_updated三个便捷封装并结合仓库源码剖析其在组件挂载、更新、卸载三个阶段的后台调度原理帮助你写出资源可回收、时序可控的声明式 Flet 应用。use_effect 解决什么问题Flet 1.0 在传统命令式 API 之外引入了声明式写法组件是一个以状态为输入、返回 UI 的纯函数每次状态变化框架都会重新执行组件函数并重渲染界面详见 Introducing Declarative UI in Flet。这种UI f(state) 的模型要求组件函数本身保持纯净——你不能在函数体内直接启动定时器、订阅事件或发起异步 IO因为这些操作无法被 React 式的重渲染机制安全地重复执行。use_effect正是为此设计的它把副作用从渲染函数体中分离出来由框架在合适的时机挂载、依赖变化、卸载统一调度执行并保证清理逻辑cleanup必然成对运行避免内存泄漏与悬挂回调。从源码看use_effect是 Flet 内置的四个核心 Hook 之一另三个为use_state、use_context、use_memo其完整实现位于 use_effect.py并通过 __init__.py 以ft.use_effect、ft.on_mounted、ft.on_unmounted、ft.on_updated对外导出。函数签名与三个核心参数use_effect的签名定义在 use_effect.py#L42-L75def use_effect( setup: Callable[[], Any | Awaitable[Any]], dependencies: Sequence[Any] | None None, cleanup: Callable[[], Any | Awaitable[Any]] | None None, )参数类型作用执行时机setupCallable可同步或异步执行副作用的主体函数若返回一个可调用对象该返回值会被自动注册为新的 cleanup组件挂载后、以及依赖变化触发重跑时dependenciesSequence[Any] \| None依赖列表提供时仅在任一依赖变化后重跑不提供None时只在初始挂载后运行一次决定 effect 是否重跑cleanupCallable可同步或异步副作用清理函数每次 effect 重跑之前、以及组件卸载时三个参数均为可选传入的惯例配合如下不传dependencies默认Noneeffect 只在组件挂载后运行一次。这是最常用的一次性初始化模式等价于 React 中的useEffect(fn, [])语义但注意语义细节见下文依赖的三种模式。传入空列表[]同样只在挂载后运行一次on_mounted即此语义。传入非空依赖列表每次渲染时框架会比较当前依赖与上一次的依赖快照任一发生变化就按先 cleanup、后 setup的顺序重跑。setup与cleanup都支持同步函数与async协程函数——从类型注解Callable[[], Any | Awaitable[Any]]可以看出异步副作用是官方一等公民异步调度细节见后台调度器一节。依赖的三种模式与触发规则模式一不传 dependencies —— 仅挂载时运行一次ft.component def App(): ft.use_effect(lambda: print(只运行一次组件挂载后)) return ft.Text(Hello)依赖为None时effect 在组件did_mount后调度执行一次后续重渲染不再触发。模式二传空列表 [] —— 同样运行一次on_mounted 的底层实现ft.use_effect(lambda: print(运行一次), dependencies[])这与模式一行为一致on_mounted就是它的语法糖。模式三传依赖列表 —— 依赖变化时重跑ft.component def App(): user_id, set_user_id ft.use_state(1) def load(): print(f加载用户 {user_id} 的数据...) ft.use_effect(load, [user_id]) return ft.Row( controls[ ft.Text(f当前用户{user_id}), ft.Button(下一个, on_clicklambda: set_user_id(user_id 1)), ], )每当user_id变化load就会重新执行user_id不变时例如点击按钮以外的重渲染effect 不会重复运行。从 component.py#L324-L346 的_run_render_effects实现可以看到精确的判定逻辑for hook in self._state.hooks: if isinstance(hook, EffectHook) and hook.deps ! []: deps_changed ( hook.deps is None or hook.prev_deps is None or hook.deps ! hook.prev_deps ) if deps_changed: if callable(hook.cleanup): self._schedule_effect(hook, is_cleanupTrue) self._schedule_effect(hook, is_cleanupFalse)关键点依赖列表为[]的 effect 在挂载后即被跳过hook.deps ! []条件因此不会被重复调度依赖为None或依赖发生变化的 effect则按先调度 cleanup、再调度 setup的顺序入队。需要留意依赖比较采用的是 Python 列表的浅比较hook.deps ! hook.prev_deps因此依赖项应是字符串、数字等可哈希/可直接比较的简单值若依赖是每次渲染都新建的列表或对象比较会认为变了而每次重跑。仓库在 utils.py 中提供了value_equal等容错比较工具其中对float(nan)做了特判NaN 视为与 NaN 相等供依赖比较等场景使用。cleanup清理旧副作用再执行新副作用cleanup解决的是副作用残留问题。典型场景是订阅外部事件流或启动定时器若在依赖变化后旧订阅仍存活会造成重复订阅和内存泄漏。ft.component def Timer(): count, set_count ft.use_state(0) running, set_running ft.use_state(True) def tick(): if running: set_count(count 1) # 每 1 秒触发一次 tick async def setup(): while True: await asyncio.sleep(1) tick() def teardown(): print(定时器已清理) ft.use_effect(setup, [running], cleanupteardown) return ft.Row( controls[ ft.Text(f计数{count}), ft.Button(暂停/继续, on_clicklambda: set_running(not running)), ], )当running变化时框架先执行teardown()再重新启动setup()当组件从页面移除卸载时teardown()也会被调用一次。除了显式传入cleanup参数外setup返回一个可调用对象也会被自动视为 cleanup。这一机制实现在 session.py#L726-L745if hook and hook.setup and not is_cleanup: hook.cancel() res None if inspect.iscoroutinefunction(hook.setup): hook._setup_task asyncio.create_task(hook.setup()) else: res hook.setup() if callable(res): hook.cleanup res elif hook and hook.cleanup and is_cleanup: hook.cancel() if inspect.iscoroutinefunction(hook.cleanup): hook._cleanup_task asyncio.create_task(hook.cleanup()) else: hook.cleanup()因此下面这种返回清理函数的写法与显式cleanup参数等价def setup(): conn open_connection() return lambda: conn.close() # 返回值成为 cleanup ft.use_effect(setup)注意优先级cleanup参数显式传入时优先使用若setup返回值本身是可调用对象它会覆盖 hook 上的 cleanup。此外同步的setup若返回None则不会改变已有的 cleanup。三个便捷封装on_mounted / on_unmounted / on_updated仓库在 use_effect.py#L78-L116 基于use_effect实现了三个语义明确的封装on_mounted —— 挂载后运行一次def on_mounted(fn: Callable[[], Any | Awaitable[Any]]) - None: use_effect(fn, dependencies[])底层是dependencies[]的use_effect。适用于读取页面尺寸、注册一次性事件、启动初始化逻辑等场景。官方示例 routing_two_pages/main.py 中即用ft.on_mounted在挂载后打印页面尺寸ft.on_mounted( lambda: print(Page size:, ft.context.page.width, ft.context.page.height) )on_unmounted —— 卸载时运行一次def on_unmounted(fn: Callable[[], Any | Awaitable[Any]]) - None: # No-op setup; only need cleanup to fire on unmount use_effect(lambda: None, dependencies[], cleanupfn)底层是一个空 setup cleanup的 effectsetup 什么都不做仅在组件卸载时触发fn。适合在组件移除前反注册事件、关闭资源def cleanup(): ft.context.page.on_route_change None # 或释放其他资源 ft.on_unmounted(cleanup)on_updated —— 每次渲染后运行可带依赖def on_updated( fn: Callable[[], Any | Awaitable[Any]], dependencies: Sequence[Any] | None None ) - None: use_effect(fn, dependenciesdependencies)dependenciesNone每次重渲染后都运行提供依赖列表仅依赖变化时运行。官方示例 routing_two_pages/main.py 用它把主题模式同步到页面def update_theme_mode(): print(Theme mode changed to:, app.theme_mode) ft.context.page.theme_mode app.theme_mode ft.on_updated(update_theme_mode, [app.theme_mode])注意on_updated在挂载后也会触发一次因为它本质上就是带依赖的普通 effect。源码级原理Hook 按位置复用与三阶段调度要理解use_effect的可靠性需要看三个层次的实现。1. Hook 状态按渲染位置持久化use_effect的第一步是获取当前正在渲染的组件component current_component() deps list(dependencies) if dependencies is not None else None hook component.use_hook( lambda: EffectHook(component, setupsetup, depsdeps, cleanupcleanup) ) hook.setup setup hook.prev_deps hook.deps hook.deps deps hook.cleanup cleanupcurrent_component()来自 utils.py#L37-L53它从渲染器Renderer的渲染栈顶取出当前组件若在组件渲染帧之外调用会抛出RuntimeError——这保证了 Hook 只能用于ft.component声明的函数式组件内。use_hook实现在 component.py#L290-L310组件内部维护一个hooks列表和一个游标hook_cursor。每次渲染时游标归零Hook 按调用顺序取回如果该位置还没有 Hook 则用工厂函数新建。这就是按位置复用——只要 Hook 的调用顺序稳定状态就能跨渲染持久化反过来在条件分支里调用 Hook 会打乱位置对应关系应当避免。EffectHook本身是Hook的子类hook.pyHook继承自ComponentOwned以eqFalse保证按对象身份比较。它保存了 setup、cleanup、deps 以及上一次的 prev_deps还有运行时的_setup_task/_cleanup_task两个asyncio.Task句柄和cancel()方法use_effect.py#L12-L40用于取消仍挂起的异步任务。2. 生命周期三阶段mount / render / unmount组件的生命周期方法会按阶段调度不同的 effectcomponent.py#L312-L383阶段触发方法行为挂载did_mount()→_run_mount_effects()遍历所有EffectHook无条件调度 setup更新update()/before_update()→_run_render_effects()跳过deps []的 effect其余按依赖变化调度 cleanup setup卸载will_unmount()→_run_unmount_effects()遍历所有EffectHook凡有 cleanup 一律调度 cleanup随后清空_state.hooks此外update()与will_unmount()中还有防僵尸保护未挂载组件收到更新请求会直接跳过避免卸载后重渲染导致泄漏。3. 后台调度器异步 setup / cleanup 的执行者调度链路为_schedule_effect(hook, is_cleanup)→context.page.session.schedule_effect(hook, is_cleanup)component.py#L222-L232。session.py#L676-L691 的schedule_effect把(hook, is_cleanup)追加到会话的待处理队列并持有强引用——注释明确说明若只存弱引用组件卸载清空_state.hooks后排队的清理 effect 就会被垃圾回收而掉在地上。随后会话后台的__updates_scheduler协程session.py#L702-L749循环消费队列先处理待更新控件再逐个执行 effect。执行时的关键行为包括运行 setup/cleanup 前先调用hook.cancel()取消上一次尚未完成的_setup_task/_cleanup_task防止旧任务与新任务并发竞争inspect.iscoroutinefunction判断协程函数异步的用asyncio.create_task调度同步的直接调用若同步 setup 返回可调用对象将其注册为 cleanup即前文所述机制effect 内抛出的异常会被捕获并经由session.error()上报给客户端避免拖垮整个调度循环。实战组合数据加载、订阅与状态同步示例带清理的异步数据加载import asyncio import flet as ft ft.component def Profile(): user_id, set_user_id ft.use_state(1) loading, set_loading ft.use_state(True) data, set_data ft.use_state() async def load(): set_loading(True) await asyncio.sleep(0.5) # 模拟网络请求 set_data(f用户 {user_id} 的数据) set_loading(False) ft.use_effect(load, [user_id]) return ft.Column( controls[ ft.ProgressRing(visibleloading), ft.Text(data), ft.Button(加载用户 2, on_clicklambda: set_user_id(2)), ], ) ft.run(lambda page: page.render(Profile))切换user_id时load自动重跑由于依赖是user_id这一简单值点击按钮以外的渲染不会触发重复请求。示例组件卸载时反注册事件避免悬挂回调ft.component def RouteListener(): def on_route(e): print(路由变化, e.route) # 挂载后订阅 ft.on_mounted(lambda: setattr(ft.context.page, on_route_change, on_route)) # 卸载时反注册 ft.on_unmounted(lambda: setattr(ft.context.page, on_route_change, None)) return ft.Text(监听路由中...)使用注意事项Hook 调用顺序必须稳定use_effect依赖按位置复用不要在if、循环中条件式调用 Hook否则依赖快照会错位。依赖用简单值依赖比较是列表浅比较每次渲染新建的列表/对象会被判定为已变化而反复触发 effect。务必成对提供清理启动定时器、订阅事件、打开连接的 effect都应在cleanup或setup返回值中释放资源未卸载的组件其 cleanup 一定会在组件销毁前被调度见_run_unmount_effects。避免在 effect 内直接修改依赖自身若setup中修改了依赖列表里的值可能造成修改→重跑→再修改的循环需要更新状态时优先走use_state的 setter或借助use_memo缓存派生值。异步副作用是受支持的setup/cleanup均可写为async def框架会以asyncio.create_task调度并在重跑/卸载前尝试取消未完成的任务EffectHook.cancel。on_updated与普通 effect 一样也会在挂载后触发一次如果只想监听变化后的更新可结合挂载标记自行判断首次调用。注意dependenciesNone与dependencies[]的细微差别在实现上二者挂载时都会运行但_run_render_effects会显式跳过deps []的 effect语义上[]更明确地表达永不重跑。总结use_effect是 Flet 声明式组件中承接一切副作用的统一入口setup描述做什么dependencies决定何时重做cleanup保证做完收尾。配合on_mounted、on_unmounted、on_updated三个封装可以精确地把逻辑挂到组件生命周期的任意节点。其背后是 Hook 按位置复用、组件三阶段调度、会话级异步调度器三层机制的协同——理解这些原理你就能在声明式 Flet 应用中写出时序可控、无泄漏的副作用代码。进一步可参考 use_effect.py 的完整源码与 component.py 的生命周期实现以及 routing_two_pages 等官方示例。【免费下载链接】fletBuild realtime web, mobile and desktop apps in Python only. No frontend experience required.项目地址: https://gitcode.com/gh_mirrors/fl/flet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表