引言
LiveView 把「页面状态」搬进 BEAM 进程:服务端持有 socket、diff 推给浏览器,JavaScript 只负责最小 patch。但真到生产,你需要搞清生命周期、跨进程状态同步、并发长任务与性能边界。本文是 LiveView 的进阶篇:状态管理、组件化、PubSub 协作与性能优化。
前置:/elixir-intro-phoenix/(Phoenix 基础)、/elixir-phoenix-channels-realtime/(Channels 与 PubSub)、/erlang-concurrency-actors/(进程并发)。
目录
- 1. LiveView 生命周期再理解
- 2. socket 状态管理与变更追踪
- 3. 函数组件与嵌套
- 4. 事件处理与表单绑定
- 5. PubSub 跨进程状态同步
- 6. 并发与长任务
- 7. 性能优化:diff 与节流
- 8. 连接恢复与离线
- 9. 常见陷阱与调试
- 10. 速查表与一句话记忆
- 延伸阅读
1. LiveView 生命周期再理解
1.1 进程模型
每个 LiveView 连接是一个进程,浏览器首次渲染 = 服务端生成 HTML,之后只推 diff:
浏览器首次请求 → mount/3 → render → 返回完整 HTML
⇅ 建立 WebSocket
后续交互 → handle_event → 改 socket → 只推变更的 DOM(diff)
1.2 关键回调
def mount(_params, _session, socket) do
# 初始状态(可在这里订阅 PubSub)
{:ok, assign(socket, :messages, [], :page, 1)}
end
def handle_event("click", _params, socket) do
# 事件处理:改状态
{:noreply, assign(socket, :page, socket.assigns.page + 1)}
end
def render(assigns) do
~H"""
<button phx-click="click">页码: <%= @page %></button>
"""
end
记忆:LiveView = 每连接一个 BEAM 进程——mount 初始化、handle_event 改 socket、render 生成 HTML;首载全量、之后只推 diff,浏览器做最小 patch。
2. socket 状态管理与变更追踪
2.1 assign 的变更追踪
assign 是 LiveView 状态的核心:只有被标记 @var 使用的部分才参与 diff。
# 一次性 assign 多个
socket = assign(socket, user: user, posts: posts)
# 变更追踪:Phoenix.Component 的 diffs 只包含变化的 assigns
def handle_event("next", _, socket) do
{:noreply, update(socket, :page, &(&1 + 1))}
end
2.2 避免大状态
✗ 把整个列表塞进 socket(每个 diff 都大)
✓ 用 @assigns 只传视图需要的字段
✓ 大列表分页/分片,WebSocket 只推变化部分
记忆:assign 是状态核心、update 增量改、diff 只推变化的 assigns——别把大列表塞 socket,按需分片分页。
3. 函数组件与嵌套
3.1 函数组件
无状态 UI 复用:
defmodule MyAppWeb.PlayerCard do
use Phoenix.Component
def player_card(assigns) do
~H"""
<div class="card">
<h3><%= @player.name %></h3>
<span>评分 <%= @player.score %></span>
</div>
"""
end
end
3.2 在页面里使用
<PlayerCard.player_card player={@player} />
3.3 组件 vs 嵌套 LiveView
| 方式 | 用途 |
|---|---|
| 函数组件 | 纯 UI 复用,无状态 |
| 嵌套 LiveView | 独立状态/生命周期(表格、输入框) |
记忆:函数组件管纯 UI 复用(use Phoenix.Component)、嵌套 LiveView 管独立状态/生命周期(输入框、局部表格)——能无状态就别嵌套。
4. 事件处理与表单绑定
4.1 phx-click 事件
<button phx-click="save" phx-value-id={@id}>保存</button>
def handle_event("save", %{"id" => id}, socket) do
# 处理保存
{:noreply, socket}
end
4.2 表单与 phx-submit
<.form for={@form} phx-submit="create" phx-change="validate">
<input name="post[title]" />
<button>提交</button>
</.form>
def handle_event("create", %{"post" => attrs}, socket) do
# 落库
{:noreply, socket}
end
4.3 防抖(phx-debounce / phx-throttle)
<input phx-debounce="300" phx-change="search" /> <!-- 输入停 300ms 才发 -->
<input phx-throttle="2000" phx-change="heartbeat" /> <!-- 最多 2s 一次 -->
记忆:事件三件套——phx-click 点按、phx-submit/phx-change 表单、phx-debounce/phx-throttle 节流搜索/心跳防刷服务端。
5. PubSub 跨进程状态同步
5.1 一个 LiveView 更新,多个客户端同步
def mount(_params, _session, socket) do
# 订阅全局 Topic
Phoenix.PubSub.subscribe(MyApp.PubSub, "counter")
{:ok, assign(socket, :count, 0)}
end
def handle_info({:counter, n}, socket) do
{:noreply, assign(socket, :count, n)}
end
# 任何进程(其他 LiveView、GenServer、Job)更新计数并广播
Phoenix.PubSub.broadcast(MyApp.PubSub, "counter", {:counter, 42})
5.2 多 LiveView 协作
LiveView A(改状态)→ PubSub.broadcast("topic", msg)
↓
LiveView B/C/D(handle_info 收到)→ 各自 update
记忆:跨进程同步 = mount 时 subscribe Topic、handle_info 里 update 状态、任何进程 broadcast 触发——一个 LiveView 改、所有订阅者同步。
6. 并发与长任务
6.1 长任务不能阻塞 LiveView 进程
LiveView 进程处理事件时阻塞 = 该连接所有交互卡住。用异步任务:
def handle_event("export", _params, socket) do
# 派发到独立进程,完成后广播回 LiveView
Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
data = MyApp.Report.generate(socket.assigns.user.id)
Phoenix.PubSub.broadcast(MyApp.PubSub, "report:" <> socket.assigns.user.id,
{:report_ready, data})
end)
{:noreply, socket}
end
def handle_info({:report_ready, data}, socket) do
{:noreply, assign(socket, :report, data)}
end
6.2 并发收集
用 Task.async_stream 并发处理列表(不占 LiveView 进程):
results =
ids
|> Task.async_stream(fn id -> MyApp.fetch(id) end,
max_concurrency: 8, timeout: 5000)
|> Enum.map(fn {:ok, r} -> r end)
记忆:LiveView 进程是单线程的——长任务必须 Task 异步化(async_nolink + 完成后 broadcast 回 handle_info);批量并发用 Task.async_stream(max_concurrency)。
7. 性能优化:diff 与节流
7.1 diff 优化
✗ 大列表每次全量重渲染(diff 巨大)
✓ 列表项用 :id 标识(Phoenix.Component 自动做最小更新)
✓ 只更新变化的 assign
<!-- 列表项带 id,LiveView 只 patch 变化的项 -->
<div id={"item-" <> item.id}>
7.2 节流与限频
# 事件太频繁(如 mouse move)→ phx-throttle
# 服务端也做保护:ETS 记录最后时间
def handle_event("pos", _, socket) do
case :ets.lookup(:pos_throttle, socket.id) do
[{_, t}] when t > now() - 200 -> {:noreply, socket} # 丢弃
_ -> :ets.insert(:pos_throttle, {socket.id, now()})
{:noreply, socket}
end
end
7.3 传输层
- WebSocket 默认压缩(
compress: true) - 大响应考虑流式传输(
send_info/ chunked)
记忆:diff 优化 = 列表项带 :id(最小 patch)+ 只更新变化 assign;事件节流用 phx-throttle + 服务端 ETS 限频双保险——把推送量与事件频率压到最小。
8. 连接恢复与离线
8.1 断线自动恢复
LiveView 内置连接恢复:WebSocket 断开后 JS 端重连,服务端进程保持:
# mount 里返回临时区数据(让恢复期 UI 有内容)
def mount(_params, _session, socket) do
{:ok, assign(socket, :messages, fetch_recent()), temporary_assigns: [messages: []]}
end
8.2 恢复策略
✓ 服务端进程保持 socket 状态(默认)
✓ temporary_assigns:大列表不常驻内存,断线恢复时重新拉
✓ 需要时用 phx-disconnected 提示
记忆:连接恢复 = JS 重连 + 服务端进程保持;temporary_assigns 让大列表不常驻内存、恢复时重拉——断线无感恢复的关键配置。
9. 常见陷阱与调试
9.1 陷阱清单
✗ 在 mount 里做慢 IO(阻塞首次渲染)→ 移到 after_connect / 异步
✗ socket 里塞大 List(diff 膨胀)→ 分片 + temporary_assigns
✗ 忘 subscribe → 广播收不到
✗ handle_event 崩溃 → 连接断开(用 try/rescue + log)
9.2 调试工具
# 开启详细日志
config :phoenix, :live_view, debug: true
# 观察 diff 变化(LiveViewDebugger 扩展)
# 用 IO.inspect 看 socket.assigns
记忆:常见陷阱——mount 慢 IO、socket 大 List、漏 subscribe、事件崩溃;调试用 live_view debug 配置 + LiveViewDebugger 扩展观察 diff。
10. 速查表与一句话记忆
| 环节 | 做法 |
|---|---|
| 初始化 | mount/3 |
| 状态 | assign / update |
| 事件 | handle_event + phx-click |
| 表单 | phx-submit + phx-change |
| 节流 | phx-debounce / phx-throttle |
| 跨进程 | PubSub subscribe + handle_info |
| 长任务 | Task.async_stream |
| 恢复 | temporary_assigns |
| 调试 | live_view debug |
一句话记忆:LiveView 进阶 = 每连接一个 BEAM 进程(mount 初始化、handle_event 改 socket、render 只推 diff);状态用 assign/update、diff 只包含变化的 assigns(列表项带 :id 做最小 patch);UI 复用函数组件、独立状态嵌套 LiveView;跨进程同步用 PubSub subscribe + handle_info,长任务必须 Task 异步化(async_nolink + 广播回来)别阻塞 LiveView 进程;事件节流 phx-throttle + 服务端限频、temporary_assigns 让大列表断线可恢复;「页面状态在服务端进程」是 LiveView 的本质。"
延伸阅读
- /elixir-intro-phoenix/ — Phoenix 基础与 LiveView 入门
- /elixir-phoenix-channels-realtime/ — Channels 与 PubSub 实时
- /erlang-concurrency-actors/ — 进程并发模型
- /elixir-ecto-data-access/ — Ecto 数据访问
- /erlang-otp-framework/ — OTP 进程与任务
- [[web]] — Web 全栈开发
- [[javascript]] — 前端交互
- Phoenix LiveView 文档
- LiveView 性能指南
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。