《elisp》 每日一练——hook 机制详解

在前面的文章中,我们完成了对于Major Mode 和 Minor Mode 的学习。今天开始我们正式进入有关hook 和 advice 机制的讨论。

背景知识

什么是 hook?我们在各种编程语言中都见过hook,我个人理解的hook就是一组程序预留的插口,我们可以利用这些插口在程序生成某种行为时能自动执行自己的代码进行干预,以便修改原程序的行为。

就像钓鱼一样,我们在对应水域下钩子,鱼群按照预定的流程在水中游动就像程序执行一样。在鱼上钩之后,作为钩子程序的钓鱼佬需要有所动作。这里的鱼群游动就是原程序,钓鱼佬就是钩子程序,下钩的构成被称为添加一个hook。

但是达到这种执行自定义代码以便修改程序行为的方式还有一种就是回调。它们二者有什么不同呢?二者都是由原程序在特定时候主动调用。但是hook比起回调来将更为灵活。回调本质上是传入一个函数,在特定的时机由原程序主动调用。而hook是系统主动留下扩展槽位,使用者可以挂载多个hook函数,而调用者会一一调用这些挂载的函数。回调更像是一对一的约定,发生事件了触发单点,而hook更像是群发,事件发生了,所有注册了的都能收到通知。

我现在也没有把握将它们很清晰的给各位读者进行区分,或者以更形象的例子让读者理解hook的概念,但是Emacs中使用hook并不需要对hook有什么很深的理解。我们只需要知道我们可以对对应事件注册一些hook,在事件发生时能调用这些hook就行了

hook 分类

Emacs中 Hook 的本质就是一个存储了函数列表的变量,当某个事件发生时Emacs会遍历这个变量里登记的所有函数。

根据调用方式不同,Emacs中的hook可以分为两类:

  • 标准hook,命名一般以 -hook 结尾。例如保存文件之后的 after-save-hook、关闭window窗口的 quit-window-hook
  • 异常hook, 命名一般以 -functions 结尾,例如 find-file-not-found-functions

这里的异常hook 只是调用方式不同,不一定代表发生了错误或者异常。

调用 hook

标准hook 中的函数不接受任何参数,函数的返回值被忽略,可以使用 run-hooks 执行。该函数的签名如下:

emacs-lisp 复制代码
(run-hooks &rest HOOKS)

参数 HOOKS 是对应的hook 变量。它会依次执行变量中注册的所有函数。

对于异常hook 来说,列表中的函数必须接收特定的参数,或者返回值会影响后续逻辑。触发这类hook 可以使用 run-hook-with-args、run-hook-with-args-until-success、run-hook-wrapped。

run-hook-with-args 可以把相同的参数传给钩子列表中的每一个函数,并依次全部执行完。它的函数原形为:

emacs-lisp 复制代码
(run-hook-with-args HOOK &rest ARGS)

run-hook-with-args-until-success 依次调用钩子中的函数,一旦某个函数返回非 nil,就立刻停止,并返回那个非 nil 值。它的函数原形为:

emacs-lisp 复制代码
(run-hook-with-args-until-success HOOK &rest ARGS)

它适合在"尝试直到有人成功"的场景下使用。

run-hook-wrapped 更底层、更灵活的运行方式。它不是直接调用钩子函数,而是把每个钩子函数交给一个包装函数去调用。

emacs-lisp 复制代码
(run-hook-wrapped HOOK WRAP-FUNCTION &rest ARGS)

它会遍历 hook 中所有函数,将函数和对应的参数都交给包装函数 WRAP-FUNCTION。直到某个包装函数返回非nil则停止。可以将它理解为 run-hook-with-args-until-success 将hook 中的函数都加了一层包装。执行逻辑与 run-hook-with-args-until-success 相同。

注册 hook

我们可以通过 add-hook 往hook 变量中注册一个函数,它的原形是

emacs-lisp 复制代码
(add-hook HOOK FUNCTION &optional DEPTH LOCAL)
  • HOOK 对应着hook 变量,存储了需要执行的函数
  • FUNCTION 要挂载的函数,它是一个符号或者lambda
  • DEPTH 控制挂载顺序,默认是 0 或者 nil 表示追加到列表头部, t 或者 90 表示追加到 Hook 列表尾部,数值越小越早执行。我们可以使用这个参数精准的控制函数执行顺序
  • LOCAL 如果为 t 表示注册的是一个buffer local 类型的hook函数,仅仅在当前buffer 中生效

删除hook 函数

从 hook 列表中删除某个函数可以使用 remove-hook,它的函数签名是

emacs-lisp 复制代码
(remove-hook HOOK FUNCTION &optional LOCAL)

它的参数与 add-hook 类似。这里就不细说了

练习

我们使用hook 来实现一个简单的保存功能,在关闭当前buffer, 也就是调用 kill-buffer 时,如果当前buffer关联了具体的文件,我们就行保存操作。

emacs-lisp 复制代码
(defun my-save-buffer-on-kill()
  "关闭buffer时,如果buffer关联了文件且已修改,则自动保存"
  (when (and buffer-file-name
			 (buffer-modified-p))
	(save-buffer)))

(add-hook 'kill-buffer-query-functions #'my-save-buffer-on-kill)

这里我们使用的是 kill-buffer-query-functions 异常hook,根据Emacs的文档,在kill-buffer 执行的时候,它会首先调用 kill-buffer-query-functions 这个hook的主要作用是检查被关闭的buffer是否没有被保存,没有的话就弹出一个提示,让用户选择是否保存或者停止关闭的操作。我们的保存函数应该先于这个动作,因此这里不适合注册到 kill-buffer-hook。

总结

我们本节简单介绍了 hook 的基本概念,在Emacs中,hook就是存储了一组函数,由Emacs在适当的时机遍历并调用它们。

今天介绍了几个函数:

  • add-hook

  • remove-hook

  • run-hooks

  • run-hook-with-args

  • run-hook-with-args-until-success

  • run-hook-wrapped

相关推荐
Masimaro9 小时前
《elisp 每日一练》——major mode 综合练习
emacs
aluluka3 天前
《elisp 每日一练》—— syntax table 与 font lock
emacs·lisp·elisp
Masimaro5 天前
《elisp 每日一练》—— 基于treesitter 的语法高亮
emacs
智购科技自动售货机工厂16 天前
数字人民币硬钱包支付失败,排查发现是NFC读卡器功率不足~YH
python·面试·架构·eclipse·emacs
Masimaro18 天前
《elisp 每日一练》—— define-derived-mode 管理 syntax-table
emacs
Masimaro25 天前
《elisp 每日一练》—— Major Mode 管理 abbrev-table
emacs
AI情绪识别开源1 个月前
检信AI学业生涯智选系统技术方案概述
emacs
Masimaro1 个月前
《elisp 每日一练》—— Minor Mode 回顾与综合练习
emacs
aluluka1 个月前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs·lisp·elisp