用eval-after-load避免不必要的elisp包的加载
Emacs中为保证操作的一致性和使用的方便性, 同一个功能在不同的mode中都绑定相同的键, 这样你操作的时候不用区分当前到底是哪个mode, 比如, c-mode, c++-mode, java-mode, awk-mode中注释都是用C-c C-c, c-mode, java-mode中都是用C-c C-q格式化当前函数, 等等. 所以我们自己在定义快捷键的时候, 最好也遵守这种惯例.
假如我们现在要对Info-mode, view-mode, grep-mode, color-theme绑定vi中的光标移动快捷键hjkl, 代码如下:
1 2 3 4 5 | (dolist (map (list Info-mode-map view-mode-map grep-mode-map color-theme-mode-map)) (define-key map "h" 'backward-char) (define-key map "l" 'forward-char) (define-key map "j" 'next-line) (define-key map "k" 'previous-line)) |
现在用C-x C-e执行上面代码, 出现以下错误:
1 2 3 4 5 6 7 8 | Debugger entered--Lisp error: (void-variable Info-mode-map) (list Info-mode-map view-mode-map grep-mode-map color-theme-mode-map) (let ((--dolist-tail-- ...) map) (while --dolist-tail-- (setq map ...) (define-key map "h" ...) (define-key map "l" ...) (define-key map "j" ...) (define-key map "k" ...) (setq --dolist-tail-- ...))) (dolist (map (list Info-mode-map view-mode-map grep-mode-map color-theme-mode-map)) (define-key map "h" (quote backward-char)) (define-key map "l" (quote forward-char)) (define-key map "j" (quote next-line)) (define-key map "k" (quote previous-line))) eval((dolist (map (list Info-mode-map view-mode-map grep-mode-map color-theme-mode-map)) (define-key map "h" (quote backward-char)) (define-key map "l" (quote forward-char)) (define-key map "j" (quote next-line)) (define-key map "k" (quote previous-line)))) eval-last-sexp-1(nil) eval-last-sexp(nil) call-interactively(eval-last-sexp nil nil) |
原因是还没有加载info这个包, Info-mode-map还没有定义, 那自然其他几个map也有这个问题, 所以我们要先加载它们对应的包, 代码变成:
1 2 3 4 5 6 7 8 9 10 | (require 'view) (require 'info) (require 'grep) (require 'color-theme) (dolist (map (list Info-mode-map view-mode-map grep-mode-map color-theme-mode-map)) (define-key map "h" 'backward-char) (define-key map "l" 'forward-char) (define-key map "j" 'next-line) (define-key map "k" 'previous-line)) |
那么又有一个问题, 你有时候打开Emacs只是写点c的代码, 并没有用view-mode, 也没有看info, 也有可能没有grep, 更有可能没用color-theme-mode, 这样就白白浪费你的时间去加载这些你有时候根本用不到的view, info, grep, color-theme, 虽然这几个包启动时间不长, 但随着你的Emacs使用年龄越来越大, 你的Emacs配置文件将会越来越长. 而且像CEDET那样的庞然大物启动起来还是有点慢的。截止到笔者做此文时, 我的.emacs文件888行, .emacs中还会加载151个settings文件, 这些文件共11654行, 总共加起来12542行, 具体文件行数大家可以下载我的DEA, 用wc命令看一下. 可想而知, 如果都是那样浪费的话, 你的Emacs将会比Eclipse启动还慢.
那现在怎么办?
Emacs早为你想好了。
Emacs中有一种数据类型叫autoload, 这种数据类型的函数定义是一个以autoload开头的list,比如你emacs -q启动Emacs,然后执行下面的代码:
(symbol-function 'org-mode) ; => (autoload "org" 1272073 t nil) |
会得到上面类似的结果。
那这种类型有什么作用呢?
减少不必要的lisp包的加载!
上面的函数定义表示当你真正执行M-x org-mode的时候,Emacs才会去org.el里面去找org-mode的定义,你如果不执行M-x org-mode的话,是不用去加载org.el的。
那这种数据类型是怎样产生的呢?
Emacs中有一个autoload的函数,该函数定义如下:
(autoload function file &optional docstring interactive type) |
第一个参数是你准备生成autoload类型的symbol,第二个参数file是该symbol的定义所在的文件,第三个参数是这个symbol的文档,第四个参数是这个symbol是否是一个命令,最后一个参数指明该symbol的类型,nil表示这个symbol是个函数,keymap表示它是个keymap,macro则表示它是个宏。比如你执行下面的代码:
(autoload 'test-mode "test" "This is a test command." t) |
这样就生成了一个autoload类型的symbol test, 然后再执行下面的代码:
(symbol-function 'test-mode) ; => (autoload "test" "This is a test command." t nil) |
就会得到上面的结果。 那为什么要写那些docstring, interactive, type这些参数呢?直接写file不就可以了吗?Emacs就会找到它的定义啦。主要原因是不用加载它们对应的文件的时候,就能用C-h f (M-x describe-function)查看它们的文档,在M-x执行命令的时候,就能用补全补到它们。
Emacs中好多命令都是autoload的,比如grep, 比如ido-mode, c-mode, python-mode, 等等等等,所以才能保证启动速度非常快。
那定义这么多的autoload, 一个一个的手工去写,岂不累死?而且这些函数已经定义过了,再用autoload重新写一遍函数定义岂不重复劳动?
没关系,Emacs自然有办法。
我想经常看写elisp代码的emacser们应该会经常看到***-mode上面会有一行“;;;###autoload”的标记吧,这是Emacs的魔法标记,它的写法是由Emacs中的变量generate-autoload-cookie来定义的,当某个函数上面有这个魔法标记后,你用update-file-autoloads或者update-directory-autoloads命令会自动生成那些autoload语句放在loaddefs.el里面,当然这个文件名是由generated-autoload-file来控制的。你可以看看文件/usr/share/emacs/23.1/lisp/loaddefs.el,这是Emacs内置的autoload, 里面有非常多的autoload函数.
搭配这个autoload, Emacs还提供了一个eval-after-load函数, 该函数定义如下:
(eval-after-load file form) |
该函数第一个参数是一个file或者是一个feature的symbol, 第二个参数是一个form, 该函数的意思就是当加载file之后, 才执行form. 我们再看一个例子:
1 2 3 4 5 6 | (eval-after-load "info" `(let ((map Info-mode-map)) (define-key map "h" 'backward-char) (define-key map "l" 'forward-char) (define-key map "j" 'next-line) (define-key map "k" 'previous-line))) |
上面这个例子的意思就是加载了info后, 才去定义info的按键, 这样就不用担心Info-mode-map没有定义了, 而且还不用加载info.
配合autoload, eval-after-load, 我们就不用加载包, 而去配置包. 举个例子, 首先用autoload定义info命令, 当你输入info命令之后, Emacs去加载info命令对应的文件info.el, 这时候eval-after-load里面的form被触发, Emacs会去eval那个form, 从而配置info.
有了auotoload和eval-after-load, 我们前面的问题就迎刃而解了. 解决方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | (require 'cl) (defun find-loadfile-by-map (map) "Find load file by MAP." (case map ('Info-mode-map "info") ('view-mode-map "view") ('grep-mode-map "grep") ('color-theme-mode-map "color-theme"))) (dolist (map `(Info-mode-map view-mode-map grep-mode-map color-theme-mode-map)) (let ((file (find-loadfile-by-map map))) (eval-after-load file `(progn (define-key ,map "h" 'backward-char) (define-key ,map "l" 'forward-char) (define-key ,map "j" 'next-line) (define-key ,map "k" 'previous-line))))) |
代码很简单, 就是根据map用find-loadfile-by-map函数去找它对应的文件, 然后调用eval-after-load. 这样你不用加载info, view, grep, color-theme就可以配置它们的按键了, 当这些文件被加载之后, 这些按键定义就会自动eval.
这种根据map定义按键的地方挺多的, 至少我的使用经验是这样的, 如果每次都要像上面那样写, 也是挺麻烦的, 所以我专门写了一个elisp包, eval-after-load.el, 让你非常方便的写类似上面那样的代码.
eval-after-load.el里面主要有这几个函数: eal-eval-by-modes, eal-eval-by-maps, eal-define-key, eal-define-keys, eal-define-keys-commonly.
eal-eval-by-modes函数定义如下:
(eal-eval-by-modes modes fun) |
modes是一个mode的list, 这个函数会根据mode查找其对应的文件, 然后用eval-after-load执行fun, fun的参数为mode, 举个例子:
1 2 3 4 5 6 | (eal-eval-by-modes ac-modes (lambda (mode) (let ((mode-name (symbol-name mode))) (when (and (intern-soft mode-name) (intern-soft (concat mode-name "-map"))) (define-key (symbol-value (am-intern mode-name "-map")) (kbd "C-c a") 'ac-start))))) |
这个例子是用在auto-complete中的, 它会根据ac-modes去配置这些mode map里面的ac-satrt命令的按键定义.
eal-eval-by-maps与eal-eval-by-modes作用类似, 只不过是根据map来进行eval-after-load的, 我来用这个函数更简洁的解决上面的例子:
1 2 3 4 5 6 7 8 | (eal-eval-by-maps `(Info-mode-map view-mode-map grep-mode-map color-theme-mode-map) (lambda (map) (setq map (symbol-value map)) (define-key map "h" 'backward-char) (define-key map "l" 'forward-char) (define-key map "j" 'next-line) (define-key map "k" 'previous-line))) |
注意:上面的map是要加引用的,因为那些map可能还没有定义,所以不能直接做为变量使用。
是不是还嫌麻烦, 用eal-define-keys来写个更简单的:
1 2 3 4 5 6 | (eal-define-keys `(Info-mode-map view-mode-map grep-mode-map color-theme-mode-map) `(("h" backward-char) ("l" forward-char) ("j" next-line) ("k" previous-line))) |
这样, 就完美了解决了上面的那个例子.
eal-define-key是定义单个的map的按键的,和eal-eval-by-maps一样,它的参数map也需要被引用起来。eal-define-keys-commonly和eal-define-keys作用一样,不过是普通的define-key,没有使用eval-after-load。
现在你肯定想知道eval-after-load.el的原理了吧, 其实很简单,就是内部定义了一个mode, map到load file的映射,它是有变量eal-loadfile-mode-maps控制的,这个变量是一个list,每个元素可以是:
- 是一个load file, 比如”view”,然后view-mode, view-mode-map自动会根据”view”拼出来,还可以是”help-mode”这样,mode, map也可以根据它拼出来
- 是一个list,该list由load file, mode, map组成,比如:
("lisp-mode" lisp-interaction-mode lisp-interaction-mode-map)
这种情况适用于mode和map无法根据load file拼出来,就像上面的例子那样。
- nil
nil的作用主要是可以根据某条件动态控制eal-loadfile-mode-maps,比如:1 2 3 4
(add-to-list 'eal-loadfile-mode-maps `("test1" ,(if (>= emacs-major-version 22) "test2")))
最后,下载eval-after-load.el

loading...
好文!解决了困扰我多天的问题!!
[回复]
ahei 回复:
四月 9th, 2010 at 5:25 上午
呵呵,谢谢
[回复]
太赞了,一直没搞明白autoload怎么写啊
[回复]
ahei 回复:
四月 9th, 2010 at 5:24 上午
呵呵,多看info就明白了,info里都有写,info简直就是linux下的百科全书阿
[回复]
Meteor Liu 回复:
四月 9th, 2010 at 7:26 上午
@ahei, info这东西好是好,可是英文的还是没有中文的顺眼啊。
像你这篇文章看一遍基本就明白了,看info看半天还迷糊
[回复]
ahei 回复:
四月 9th, 2010 at 7:27 上午
@Meteor Liu, 是的,大家有空一起来翻译info吧。
[回复]
感谢作者.!!!!
[回复]
eval-after-load它们都不是autoload,怎么就可以直接调用?
还发现用update-file-autoloads或者update-directory-autoloads生成的loaddefs.el是在编译的emacs的源文件下,总感觉;;;###autoload始终没法使用,请指教。
[回复]
ahei 回复:
四月 23rd, 2010 at 8:40 上午
@, eval-after-load是subr.el里面的,这些函数以及c写的”primitive function”应该是可以直接使用的,当然具体机制我也还不清楚。最近正在打算看下info了解下emacs的启动过程,准备再写篇文章,只是最近比较忙,不知道什么时候能完成。你自己可以指定generated-autoload-file为具体的全路径名的。
[回复]
我以为primitive function都是`C source code’的。
你的意思是说指定到/usr/share/emacs/23.1.90/lisp/loaddefs.el,就可以在emacs -q后直接用命令了?
[回复]
ahei 回复:
四月 23rd, 2010 at 9:10 上午
我刚才写错了,你再看看,”primitive function”确实都是c写的,info:
A “primitive function” is a function callable from Lisp but written in
the C programming language.
emacs的loaddefs.el是emacs自己加载的,不需要你指定的阿
[回复]
匿名 回复:
四月 23rd, 2010 at 9:37 上午
@ahei, 我怎么觉得subr.el特怪异,emacs -q后,里面的函数、变量就可以直接查看,就像已经load了一样。
[回复]
ahei 回复:
四月 23rd, 2010 at 10:05 上午
@, 对阿,我刚才说了阿,这些是其他机制保证加载它们的,我目前也不清楚,呵呵,以后搞明白了再写文章。
[回复]
匿名 回复:
四月 23rd, 2010 at 10:13 上午
@ahei, 我是新手,期待……
[回复]
确实是好文,看完后我还有个疑惑,
在.emacs 中如果有:
(require ‘view)
(require ‘info)
(require ‘grep)
(require ‘color-theme) 难道emacs会去加载对应的文件吗?我一直没有搞明白autoload与require的区别。
[回复]
ahei 回复:
五月 31st, 2010 at 11:36 上午
@xilbert, load每次都会加载,require则是已经加载过了就不加载,否则就加载,autoload则是调用的时候再require
[回复]
xilbert 回复:
五月 31st, 2010 at 11:48 上午
@ahei, 太精辟了,短短一句话道出本质,谢谢
[回复]
wgf4242 回复:
九月 14th, 2010 at 11:47 下午
@xilbert,
现在回头看这文章.感觉又有收获了.
[回复]
ahei 回复:
九月 14th, 2010 at 11:54 下午
@wgf4242, 呵呵,有收获就好
[回复]
我增加了matlab mode,在.myemacs中
但是我发现有些快捷键比如C-h删除一个字符的快捷键没有被映射到matlabmode 中来,原因是matlab mode中定义了C-h这个快捷键,我怎么能把他给覆盖了呢,因为编辑习惯不一致很讨厌,我删除的时候下意识按C-h
但是却出来帮助的提示很讨厌,但是也没搞清楚怎么用del-define-keys来覆盖这个matlab mode似乎已经覆盖了,只不过C-h又被重新定义了。
[回复]
ahei 回复:
一月 26th, 2011 at 7:01 下午
你用define-key在matlab mode map中重新定义一下C-h不就ok了吗?
[回复]
Took me time for you to read all of the comments, but I seriously enjoyed the content. It turned out to be Quite helpful to me and that i am certain to all the commenters here It is always nice when you are able not only learn, but additionally entertained Im sure youd fun writing this post.
[回复]
Hi,
ahei老大,你好,这篇文章写得很好,解决了我对于require,eval-after-load一直以来的疑问,但是我有另外一个小疑问,就是:
假设在file.el里面定义了一个函数test-file,而且把test-file定义成了autoload类型,那是不是就算是我去eval (require ‘file)也不会加载test-file,而是一定要等到我第一次调用test-file时才会去加载?
期待回复!
Best Regards,
Kelvin
[回复]