[译]Emacs Loading Process

原文是 Spacemacs 的文档,描述了如何创建一个 layer。原文很长,本文只是其中关于 Emacs 加载机制的一部分,单独拿出来翻译。

原文地址: The Emacs loading process.


Emacs Lisp files

Emacs Lisp 文件包含了可以被求值的代码。在被求值后,文件中定义的函数、宏和 modes 都会在当前的 Emacs session 中可用。这也被称谓加载文件。

文件加载需要注意的问题有:

  • 加载所有需要的文件
  • 按正确的顺序加载
  • 按需加载(不要加载太多)

Loading a file

最简单的方式是调用 load-file.

1
(load-file "~/elisp/foo.el")

这种方式非常原始:

  • 路径必须准确。
  • 文件不必在 Emacs 的 load path 里。
  • 不会查找 .elc 文件,它只是简单地加载你指定的文件。

Features

更好地方式是使用 features. 一个 feature 就是一个符号(symbol),通常和它所 在的文件名相同。假如你有一个文件 my-feature.el 包含如下内容:

1
2
3
;; Your code goes here ...

(provide 'my-feature)

要使 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
2
3
4
;;;###autoload
(defun my-function ()
;; Source code...
)

;;;###autoload 这个特殊注释告诉 Emacs 下面的定义应该使用 auto-load 方式加载。 这会自动生成一个 autoload 调用。

基本上,所有“可定义”(definable)可以使用 auto-load 机制,如 function, macros, major 或 minor modes, groups, classes 等等。

这种注释也可以作用在其他东西上,如变量定义(defvar),但在这种情况下,定义只 是简单地逐字拷贝到 auto-loading 文件里。例如,下面的代码会在启动时加载 Helm, 远比文件求值的时间早,这可能不是你想要的:

1
2
;;;###autoload
(require 'helm)

确保 package 能被正确地 auto-load 是作者的责任,大多数 package 都做得很好。

Spacemacs 大量使用了 auto-loading。在 Spacemacs 中几乎所有东西都是需要时才加 载的。

Eval after load1

很多时候我们都需要在加载完 package 之后设置它们,如设置一些变量或调用一些函数。 在使用 require 这不是一个问题,因为它立即就被加载了,但使用 auto-loading 时 就会更复杂,因为配置的代码也必须推迟执行。

Emacs 为此提供了 with-eval-after-load, 用法如下:

1
2
3
(with-eval-after-load 'helm
;; Code
)

这会使得相关代码在 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
2
(use-package helm
:defer t)

这里的 defer 用了 auto-load 的基础设施和 Helm 源文件中的 autoload 命令,它 实际上是一个空操作。

1
2
3
4
5
6
7
(use-package helm
:defer t
:init
;; Code to execute before Helm is loaded
:config
;; Code to execute after Helm is loaded
)

这个 form 包含了 Helm 加载前后要执行的代码。其中 :init 部分可以被立即执行, 但因为 Helm 被延迟加载了, :config 部分的代码直到 Helm 加载后都会执行。这实 际上等同于先运行 :init 块,再将 :config 块添加到 with-eval-after-load 中。

1
2
(use-package helm
:commands (helm-find-files helm-M-x))

如果你发现 package 作者大意了,可以使用上面的代码会为命令新建一个 auto-load 引用。

1
2
(use-package ruby-mode
:mode "\\.rb\\'")

对于提供 major mode 的 package,人可以使用 :mode 关键字将扩展名和 mode 联系 起来。这会在 auto-mode-alist 中添加一个条目并为 ruby-mode 添加一个 auto-load。通常情况下不需要这么做,因为 ruby-mode 应该已经是 auto-loadable 了,且该 package 也应该自己建立了和 Ruby 文件的联系了。

Footnotes:

1

译注:如果你恰好还知道 eval-after-load, 它们的区别可以参考这个回答 What is “with-eval-after-load” in Emacs Lisp.

Last Updated 2018-08-24 Fri 01:12.
Render by hexo-renderer-org with Emacs 25.2.2 (Org mode 9.1.13)
0%