本页大纲

Hook

什么是 Hook

Hook 是一种在既有执行流程中预留扩展点(Extension Point)的设计机制。主程序先定义稳定、可复用的核心流程,再在关键节点开放接入能力,让外部逻辑在特定时机参与执行

Hook 的关键不在于”多调用了一个函数”,而在于主流程在设计阶段就明确了可扩展边界。外部代码不需要侵入主流程,只要注册到对应 Hook 点即可被自动触发。即使没有任何扩展函数,主程序也应当能独立运行;Hook 只是为系统增加了一条可控、可演进的扩展通道

Hook 流程

hook_flow

一个完整的 Hook 机制通常包含三部分:Hook 点、注册机制、触发机制

  1. Hook 点(Hook Point):主程序在执行链路中预留的扩展时机,例如“epoch 开始前”“batch 结束后”“组件挂载后”
  2. 注册机制(Register):外部逻辑把自定义 Hook 函数绑定到某个 Hook 点
  3. 触发机制(Trigger):主程序运行到指定时机时,按约定调用已注册的 Hook 函数

执行顺序可以概括为:主程序掌握流程控制权 → 在关键节点触发 Hook → Hook 执行完成后将控制权归还主程序 → 主流程继续向后执行。 这个模型保证了“主流程稳定、扩展逻辑可插拔”

Hook 与控制反转

Hook 的本质是一种以扩展点为中心的控制反转(IoC)

外部代码不主动驱动主流程,而是声明”我希望在某个时机被调用”;真正的调用时机和调用顺序由主程序统一控制。这种模式能显著降低主流程的耦合度。核心链路只关注”必须完成的主任务”,通知、日志、埋点、风控、积分、优惠券等附加能力则通过 Hook 独立接入,实现”能力可叠加、主流程不膨胀”

最简单的 Hook 示例

一个按钮点击计数器,在点击前后执行自定义逻辑:

python
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 点,让用户在不修改主流程的情况下添加监控、日志、梯度处理等扩展逻辑

python
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)
python
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

vue
<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 会提升扩展性,但也会让执行路径更分散。为了让系统可维护,通常需要提前约定以下规则:

  1. 可见性:阅读代码时不能只看主流程,还要知道哪些 Hook 会在运行时被触发,建议保留统一的 Hook 清单或文档
  2. 顺序性:同一 Hook 点注册多个函数时,应明确执行顺序(如按优先级、注册顺序),避免扩展逻辑相互踩踏
  3. 异常策略:某个 Hook 失败时是中断主流程还是记录后继续,必须由框架或机制层给出一致约定
  4. 职责边界:Hook 更适合做扩展与横切关注点(日志、监控、鉴权、资源管理),不宜承载过重的主业务逻辑

总结

Hook 是一种以扩展点为中心的设计机制:

  • 主流程稳定 - 核心逻辑不因扩展需求而膨胀
  • 扩展可插拔 - 新功能通过注册 Hook 独立接入
  • 职责分离 - 主逻辑、监控、日志、资源管理各司其职

Hook 的价值不在于”多调用了一个函数”,而在于让扩展有边界、让演进可持续。当主流程稳定、扩展点清晰、约束策略明确时,Hook 才能真正提升系统的长期可维护性

附录 A:PyTorch Lightning 完整生命周期对比

完整的训练流程 Hook 示例,展示所有可用的生命周期钩子

python
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)
python
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_backwardon_after_backward 用于观察反向传播过程
  • on_before_optimizer_step 常用于梯度裁剪、梯度统计或参数更新前的一致性校验

附录 B:Vue 完整生命周期 Hook

Vue 3 中所有可用的生命周期 Hook 及其使用场景

创建阶段

Vue 3 的 <script setup lang="ts"> 在组件创建阶段执行。此时适合声明响应式状态、计算属性、方法和生命周期 Hook,但不适合访问真实 DOM

vue
<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 的第三方插件

vue
<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

vue
<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 连接

vue
<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> 包裹时,可以使用 onActivatedonDeactivated 监听组件的激活和停用

vue
<script setup lang="ts">
import { onActivated, onDeactivated } from "vue"

onActivated(() => {
  console.log("组件被激活(从缓存中恢复)")
})

onDeactivated(() => {
  console.log("组件被停用(进入缓存)")
})
</script>

生命周期 Hook 总结:

  • 创建阶段onBeforeMount - 组件即将挂载
  • 挂载阶段onMounted - 组件已挂载,可访问 DOM
  • 更新阶段onBeforeUpdateonUpdated - 响应式数据变化触发
  • 卸载阶段onBeforeUnmountonUnmounted - 组件卸载,清理资源
  • Keep-aliveonActivatedonDeactivated - 缓存组件的激活/停用