Hook
什么是 Hook
Hook 是一种在既有执行流程中预留扩展点(Extension Point)的设计机制。主程序先定义稳定、可复用的核心流程,再在关键节点开放接入能力,让外部逻辑在特定时机参与执行
Hook 的关键不在于”多调用了一个函数”,而在于主流程在设计阶段就明确了可扩展边界。外部代码不需要侵入主流程,只要注册到对应 Hook 点即可被自动触发。即使没有任何扩展函数,主程序也应当能独立运行;Hook 只是为系统增加了一条可控、可演进的扩展通道
Hook 流程

一个完整的 Hook 机制通常包含三部分:Hook 点、注册机制、触发机制
- Hook 点(Hook Point):主程序在执行链路中预留的扩展时机,例如“epoch 开始前”“batch 结束后”“组件挂载后”
- 注册机制(Register):外部逻辑把自定义 Hook 函数绑定到某个 Hook 点
- 触发机制(Trigger):主程序运行到指定时机时,按约定调用已注册的 Hook 函数
执行顺序可以概括为:主程序掌握流程控制权 → 在关键节点触发 Hook → Hook 执行完成后将控制权归还主程序 → 主流程继续向后执行。 这个模型保证了“主流程稳定、扩展逻辑可插拔”
Hook 与控制反转
Hook 的本质是一种以扩展点为中心的控制反转(IoC):
外部代码不主动驱动主流程,而是声明”我希望在某个时机被调用”;真正的调用时机和调用顺序由主程序统一控制。这种模式能显著降低主流程的耦合度。核心链路只关注”必须完成的主任务”,通知、日志、埋点、风控、积分、优惠券等附加能力则通过 Hook 独立接入,实现”能力可叠加、主流程不膨胀”
最简单的 Hook 示例
一个按钮点击计数器,在点击前后执行自定义逻辑:
class Button:
def __init__(self):
self.count = 0
self.before_click_hooks = []
self.after_click_hooks = []
def register_before_click(self, hook):
self.before_click_hooks.append(hook)
def register_after_click(self, hook):
self.after_click_hooks.append(hook)
def click(self):
# 触发 before hooks
for hook in self.before_click_hooks:
hook(self.count)
# 主逻辑
self.count += 1
# 触发 after hooks
for hook in self.after_click_hooks:
hook(self.count)
# 使用
btn = Button()
btn.register_before_click(lambda c: print(f”点击前: {c}”))
btn.register_after_click(lambda c: print(f”点击后: {c}”))
btn.click() # 输出: 点击前: 0 \n 点击后: 1这个例子展示了 Hook 的三要素:Hook 点(before/after click)、注册机制(register 方法)、触发机制(循环调用)。主逻辑 self.count += 1 保持简洁,扩展逻辑通过 Hook 独立添加
实战:训练流程中的 Hook
PyTorch Lightning 在训练流程中预留了多个 Hook 点,让用户在不修改主流程的情况下添加监控、日志、梯度处理等扩展逻辑
import lightning as L
import torch
from torch import nn
class LitClassifier(L.LightningModule):
def __init__(self):
super().__init__()
self.model = nn.Linear(10, 2)
self.loss_fn = nn.CrossEntropyLoss()
def training_step(self, batch, batch_idx):
# 核心训练逻辑
x, y = batch
logits = self.model(x)
loss = self.loss_fn(logits, y)
self.log("train_loss", loss)
return loss
def on_train_epoch_start(self):
print(f"第 {self.current_epoch} 个 epoch 开始")
def on_before_optimizer_step(self, optimizer):
# 梯度裁剪
total_norm = torch.nn.utils.clip_grad_norm_(
self.parameters(), max_norm=1.0
)
print(f"梯度范数: {total_norm:.4f}")
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=1e-3)import torch
from torch import nn
def fit(model, train_dataloader, optimizer, loss_fn, max_epochs, device):
model.to(device)
for epoch in range(max_epochs):
model.train()
print(f"第 {epoch} 个 epoch 开始")
for batch_idx, batch in enumerate(train_dataloader):
x, y = batch
x, y = x.to(device), y.to(device)
logits = model(x)
loss = loss_fn(logits, y)
print(f"train_loss: {loss.item():.4f}")
optimizer.zero_grad()
loss.backward()
# 梯度裁剪
total_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(), max_norm=1.0
)
print(f"梯度范数: {total_norm:.4f}")
optimizer.step()对比要点:
- 有 Hook:核心训练逻辑在
training_step中保持简洁,监控、日志、梯度处理通过 Hook 独立接入 - 无 Hook:所有逻辑混在训练循环中,新增需求需要修改主流程
training_step 承担核心训练计算(前向、loss、指标记录);其他 Hook 更适合承载监控、调试、日志、资源管理等辅助职责。实践中应避免把主计算拆散到多个 Hook 中,否则会削弱代码可读性
完整的生命周期 Hook 对比(包含 on_train_start、on_train_batch_start、on_before_backward 等)见附录 A
Hook 典型应用:前端组件生命周期
Hook 广泛存在于前端框架、训练框架、Web 服务中间件、插件系统等场景。前端框架的组件生命周期 Hook 是最直观的例子:框架负责主流程,业务代码在关键阶段按需接入
Vue 组件有固定生命周期:创建 → 挂载 → 更新 → 卸载。框架负责流程推进,业务代码通过 Hook 接入相应阶段
最常用的两个 Hook
<script setup lang=”ts”>
import { ref, onMounted, onUnmounted } from “vue”
type User = {
id: string
name: string
}
const users = ref<User[]>([])
let timer: ReturnType<typeof window.setInterval> | undefined
onMounted(async () => {
// 组件挂载后:请求数据、初始化资源
const res = await fetch(“/api/users”)
users.value = await res.json()
timer = window.setInterval(() => {
console.log(“轮询中”)
}, 1000)
})
onUnmounted(() => {
// 组件卸载前:清理资源
if (timer) {
window.clearInterval(timer)
}
})
</script>
<template>
<ul>
<li v-for=”user in users” :key=”user.id”>
{{ user.name }}
</li>
</ul>
</template>这体现了 Hook 的对称关系:资源在 onMounted 创建,在 onUnmounted 释放。onMounted 是最常用的生命周期 Hook,语义明确:组件已出现在页面中,可以安全执行依赖浏览器环境的逻辑
完整的 Vue 生命周期 Hook(包含 onBeforeMount、onBeforeUpdate、onUpdated 等)见附录 B
Hook 的注意事项
Hook 会提升扩展性,但也会让执行路径更分散。为了让系统可维护,通常需要提前约定以下规则:
- 可见性:阅读代码时不能只看主流程,还要知道哪些 Hook 会在运行时被触发,建议保留统一的 Hook 清单或文档
- 顺序性:同一 Hook 点注册多个函数时,应明确执行顺序(如按优先级、注册顺序),避免扩展逻辑相互踩踏
- 异常策略:某个 Hook 失败时是中断主流程还是记录后继续,必须由框架或机制层给出一致约定
- 职责边界:Hook 更适合做扩展与横切关注点(日志、监控、鉴权、资源管理),不宜承载过重的主业务逻辑
总结
Hook 是一种以扩展点为中心的设计机制:
- 主流程稳定 - 核心逻辑不因扩展需求而膨胀
- 扩展可插拔 - 新功能通过注册 Hook 独立接入
- 职责分离 - 主逻辑、监控、日志、资源管理各司其职
Hook 的价值不在于”多调用了一个函数”,而在于让扩展有边界、让演进可持续。当主流程稳定、扩展点清晰、约束策略明确时,Hook 才能真正提升系统的长期可维护性
附录 A:PyTorch Lightning 完整生命周期对比
完整的训练流程 Hook 示例,展示所有可用的生命周期钩子
import lightning as L
import torch
from torch import nn
class LitClassifier(L.LightningModule):
def __init__(self):
super().__init__()
self.model = nn.Linear(10, 2)
self.loss_fn = nn.CrossEntropyLoss()
def on_train_start(self):
print("训练开始:初始化日志、计数器或监控状态")
def on_train_epoch_start(self):
print(f"第 {self.current_epoch} 个 epoch 开始")
def on_train_batch_start(self, batch, batch_idx):
x, y = batch
print(f"即将处理 batch {batch_idx}, 输入形状: {x.shape}")
def training_step(self, batch, batch_idx):
x, y = batch
logits = self.model(x)
loss = self.loss_fn(logits, y)
self.log("train_loss", loss)
return loss
def on_before_backward(self, loss):
print(f"反向传播前,当前 loss: {loss.item():.4f}")
def on_after_backward(self):
print("反向传播完成,可以检查梯度")
def on_before_optimizer_step(self, optimizer):
total_norm = torch.nn.utils.clip_grad_norm_(
self.parameters(),
max_norm=1.0,
)
print(f"优化器更新前,梯度范数: {total_norm:.4f}")
def on_train_batch_end(self, outputs, batch, batch_idx):
print(f"batch {batch_idx} 训练结束")
def on_train_epoch_end(self):
print(f"第 {self.current_epoch} 个 epoch 结束")
def on_train_end(self):
print("训练结束:保存统计信息或释放资源")
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=1e-3)import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Linear(10, 2)
def forward(self, x):
return self.model(x)
def fit(model, train_dataloader, optimizer, loss_fn, max_epochs, device):
model.to(device)
# 对应 on_train_start
print("训练开始:初始化日志、计数器或监控状态")
for epoch in range(max_epochs):
model.train()
# 对应 on_train_epoch_start
print(f"第 {epoch} 个 epoch 开始")
for batch_idx, batch in enumerate(train_dataloader):
x, y = batch
x = x.to(device)
y = y.to(device)
# 对应 on_train_batch_start
print(f"即将处理 batch {batch_idx}, 输入形状: {x.shape}")
# 对应 training_step
logits = model(x)
loss = loss_fn(logits, y)
# 对应 self.log("train_loss", loss)
train_loss = loss.item()
print(f"train_loss: {train_loss:.4f}")
# 清空上一轮梯度
optimizer.zero_grad()
# 对应 on_before_backward
print(f"反向传播前,当前 loss: {loss.item():.4f}")
# 对应 backward(loss)
loss.backward()
# 对应 on_after_backward
print("反向传播完成,可以检查梯度")
# 对应 on_before_optimizer_step
total_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0,
)
print(f"优化器更新前,梯度范数: {total_norm:.4f}")
# 对应 optimizer.step()
optimizer.step()
# 对应 on_train_batch_end
print(f"batch {batch_idx} 训练结束")
# 对应 on_train_epoch_end
print(f"第 {epoch} 个 epoch 结束")
# 对应 on_train_end
print("训练结束:保存统计信息或释放资源")Hook 使用建议:
on_train_batch_start适合做数据检查on_before_backward与on_after_backward用于观察反向传播过程on_before_optimizer_step常用于梯度裁剪、梯度统计或参数更新前的一致性校验
附录 B:Vue 完整生命周期 Hook
Vue 3 中所有可用的生命周期 Hook 及其使用场景
创建阶段
Vue 3 的 <script setup lang="ts"> 在组件创建阶段执行。此时适合声明响应式状态、计算属性、方法和生命周期 Hook,但不适合访问真实 DOM
<script setup lang="ts">
import { ref, computed, onBeforeMount } from "vue"
type User = {
id: string
name: string
}
const users = ref<User[]>([])
const userCount = computed(() => users.value.length)
onBeforeMount(() => {
console.log("组件即将挂载,但 DOM 尚未插入")
})
</script>
<template>
<p>用户数量:{{ userCount }}</p>
</template>挂载阶段
挂载阶段表示组件已与真实 DOM 建立联系。此时适合请求首屏数据、初始化图表、读取 DOM 尺寸,或注册依赖 DOM 的第三方插件
<script setup lang="ts">
import { ref, onMounted } from "vue"
type User = {
id: string
name: string
}
const users = ref<User[]>([])
onMounted(async () => {
const res = await fetch("/api/users")
users.value = await res.json()
})
</script>
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>更新阶段
更新阶段发生在响应式数据变化之后。Vue 会先根据新状态重新渲染 DOM,再触发更新相关 Hook
<script setup lang="ts">
import { ref, onBeforeUpdate, onUpdated } from "vue"
const count = ref<number>(0)
onBeforeUpdate(() => {
console.log("DOM 即将更新")
})
onUpdated(() => {
console.log("DOM 已经根据最新状态完成更新")
})
</script>
<template>
<button @click="count++">
{{ count }}
</button>
</template>注意: onUpdated 不应无条件修改响应式数据,否则可能触发循环更新。真正的业务状态变更更适合放在事件处理函数、计算属性或组合式函数中
卸载阶段
卸载阶段表示组件即将从页面中移除。这个阶段最适合清理资源,例如取消定时器、移除事件监听、关闭 WebSocket 连接
<script setup lang="ts">
import { onMounted, onBeforeUnmount, onUnmounted } from "vue"
let timer: ReturnType<typeof window.setInterval> | undefined
onMounted(() => {
timer = window.setInterval(() => {
console.log("定时任务执行中")
}, 1000)
})
onBeforeUnmount(() => {
console.log("组件即将卸载")
})
onUnmounted(() => {
if (timer) {
window.clearInterval(timer)
}
console.log("组件已卸载,资源已清理")
})
</script>Keep-alive 专用 Hook
当组件被 <keep-alive> 包裹时,可以使用 onActivated 和 onDeactivated 监听组件的激活和停用
<script setup lang="ts">
import { onActivated, onDeactivated } from "vue"
onActivated(() => {
console.log("组件被激活(从缓存中恢复)")
})
onDeactivated(() => {
console.log("组件被停用(进入缓存)")
})
</script>生命周期 Hook 总结:
- 创建阶段:
onBeforeMount- 组件即将挂载 - 挂载阶段:
onMounted- 组件已挂载,可访问 DOM - 更新阶段:
onBeforeUpdate、onUpdated- 响应式数据变化触发 - 卸载阶段:
onBeforeUnmount、onUnmounted- 组件卸载,清理资源 - Keep-alive:
onActivated、onDeactivated- 缓存组件的激活/停用
