原文是 Spacemacs 的文档,描述了如何创建一个 layer。原文很长,本文只是其中关于 Emacs 加载机制的一部分,单独拿出来翻译。
原文地址: The Emacs loading process.
Emacs Lisp files
Loading a file
最简单的方式是调用 load-file
.
1 | (load-file "~/elisp/foo.el") |
这种方式非常原始:
- 路径必须准确。
- 文件不必在 Emacs 的 load path 里。
- 不会查找
.elc
文件,它只是简单地加载你指定的文件。
Features
更好地方式是使用 features
. 一个 feature
就是一个符号(symbol),通常和它所
在的文件名相同。假如你有一个文件 my-feature.el
包含如下内容:
1 | ;; Your code goes here ... |
要使 Emacs 加载这个文件,像下面这样调用 require
:
1 | require 'my-feature |
这会先检查 my-feature
是否已经被加载,如果没有,就查找名字类似于
my-feature.el
, my-feature.elc
之类的文件。如果找到了,就会进行加载。当执
行完 provide
语句之后,这个 feature
就会被添加到已加载列表里,以后再遇到
require
的调用就不会再重复操作。如果相应的文件找不到就会报错。
文件 my-feature.el
中也可以包含 require
调用,这是保证代码的依赖在执行前
被加载的常用方式。
The load path
当调用 require
时,Emacs 会查找 load path
中的文件。这只不过是一个 emacs-lisp
文件的路径列表,可以在 Spacemacs 中通过 SPC h d v load-path
(或 C-h v
load-path
)来进行查看。要添加一个路径,直接向这个列表中 push 就行:
1 | (push "/some/path/" load-path) |
Auto-loading
调用 require
不过是 load-file
更高级的调用,它解决了文件需要按顺序加载的
问题,也部分解决了查找文件的问题,但是如果在启动时进行了太多 require
调用仍
然会拖慢 Emacs 的启动速度。
Emacs 使用 auto-loading 来解决这个问题。当一个函数被注册为 auto-loading,会提 供一个“空”定义。当该函数被调用时,提供这个函数的文件(以及它依赖的 feature) 会立即被加载。最后,“空”函数会被替换为真实的并被正常调用。用户只会在第一次调 用这个函数时感觉到短暂的延迟,之后对该函数(以及其他在此过程中被加载的函数) 的调用会和正常调用一样快。
要将一个函数注册为 auto-loadable,需要使用 autoload
:
1 | (autoload 'some-function "some-file") |
这行代码会使得 some-function
被调用的时候,先加载 some-file.el
再继续。
在执行完上面一行代码后,使用 SPC h d f some-function
来检查 some-function
时,它会提示这是一个 auto-loaded 函数,在被加载之前只能知道这么多信息。调用
autoload
时也可以提供更多的信息,如 doc-string,或者该函数能否被交互调用等。
这样就可以在不真正加载这个文件的情况下提供给用户更多的信息。
打开 elpa
目录,进入 helm
并查看文件 helm-autoloads.el
。里面包含了所有
Helm 文件的 auto-loads。不过这个文件不是手写的,而是通过 Helm 源文件中的特殊
注释自动生成的。用法为:
1 | ;;;###autoload |
;;;###autoload
这个特殊注释告诉 Emacs 下面的定义应该使用 auto-load 方式加载。
这会自动生成一个 autoload
调用。
基本上,所有“可定义”(definable)可以使用 auto-load 机制,如 function, macros, major 或 minor modes, groups, classes 等等。
这种注释也可以作用在其他东西上,如变量定义(defvar
),但在这种情况下,定义只
是简单地逐字拷贝到 auto-loading 文件里。例如,下面的代码会在启动时加载 Helm,
远比文件求值的时间早,这可能不是你想要的:
1 | ;;;###autoload |
确保 package 能被正确地 auto-load 是作者的责任,大多数 package 都做得很好。
Spacemacs 大量使用了 auto-loading。在 Spacemacs 中几乎所有东西都是需要时才加 载的。
Eval after load1
很多时候我们都需要在加载完 package 之后设置它们,如设置一些变量或调用一些函数。
在使用 require
这不是一个问题,因为它立即就被加载了,但使用 auto-loading 时
就会更复杂,因为配置的代码也必须推迟执行。
Emacs 为此提供了 with-eval-after-load
, 用法如下:
1 | (with-eval-after-load 'helm |
这会使得相关代码在 Helm 加载后执行(不管是通过 require
还是 autoload
加
载),如果 Helm 已经加载过了,这些代码就会立即执行。
因为 with-eval-after-load
是一个宏而非函数,所以它的参数不必加 quote。
Use-package
对于想要拼凑出高效配置的用户, use-package
是一个非常有用的 package,它提供
了一个同名的宏 use-package
, 在把整个流程串起来这方面做得不错。
推荐 layer 作者看一下 use-package
的文档。下面是一些例子。
1 | (use-package helm) |
上面的代码只是简单地加载 Helm。实际上等同于 (require 'helm)
.
1 | (use-package helm |
这里的 defer 用了 auto-load 的基础设施和 Helm 源文件中的 autoload
命令,它
实际上是一个空操作。
1 | (use-package helm |
这个 form 包含了 Helm 加载前后要执行的代码。其中 :init
部分可以被立即执行,
但因为 Helm 被延迟加载了, :config
部分的代码直到 Helm 加载后都会执行。这实
际上等同于先运行 :init
块,再将 :config
块添加到 with-eval-after-load
中。
1 | (use-package helm |
如果你发现 package 作者大意了,可以使用上面的代码会为命令新建一个 auto-load 引用。
1 | (use-package ruby-mode |
对于提供 major mode 的 package,人可以使用 :mode
关键字将扩展名和 mode 联系
起来。这会在 auto-mode-alist
中添加一个条目并为 ruby-mode
添加一个
auto-load。通常情况下不需要这么做,因为 ruby-mode
应该已经是 auto-loadable
了,且该 package 也应该自己建立了和 Ruby 文件的联系了。
Footnotes:
译注:如果你恰好还知道 eval-after-load
, 它们的区别可以参考这个回答
What is “with-eval-after-load” in Emacs Lisp.