首页 > Elisp, 中级 > 用eval-after-load避免不必要的elisp包的加载

用eval-after-load避免不必要的elisp包的加载

2010年4月9日 ahei 发表评论 阅读评论

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

分享家:Addthis中国
GD Star Rating
loading...
用eval-after-load避免不必要的elisp包的加载, 8.6 out of 10 based on 11 ratings 标签:autoload, C/C++, CEDET, eclipse, ede, Emacs, emacser, eval-after-load, keymap, lambda, org, python, theme, 光标, 补全, 配色, 配色

相关日志

分类: Elisp, 中级
  1. 2010年4月9日04:45 | #1

    好文!解决了困扰我多天的问题!!

    [回复]

    ahei 回复:

    呵呵,谢谢

    [回复]

  2. Meteor Liu
    2010年4月9日05:10 | #2

    太赞了,一直没搞明白autoload怎么写啊

    [回复]

    ahei 回复:

    呵呵,多看info就明白了,info里都有写,info简直就是linux下的百科全书阿

    [回复]

    Meteor Liu 回复:

    @ahei, info这东西好是好,可是英文的还是没有中文的顺眼啊。
    像你这篇文章看一遍基本就明白了,看info看半天还迷糊

    [回复]

    ahei 回复:

    @Meteor Liu, 是的,大家有空一起来翻译info吧。

    [回复]

  3. 匿名
    2010年4月11日06:37 | #3

    感谢作者.!!!!

    [回复]

  4. 匿名
    2010年4月23日08:33 | #4

    eval-after-load它们都不是autoload,怎么就可以直接调用?
    还发现用update-file-autoloads或者update-directory-autoloads生成的loaddefs.el是在编译的emacs的源文件下,总感觉;;;###autoload始终没法使用,请指教。

    [回复]

    ahei 回复:

    @, eval-after-load是subr.el里面的,这些函数以及c写的”primitive function”应该是可以直接使用的,当然具体机制我也还不清楚。最近正在打算看下info了解下emacs的启动过程,准备再写篇文章,只是最近比较忙,不知道什么时候能完成。你自己可以指定generated-autoload-file为具体的全路径名的。

    [回复]

  5. 匿名
    2010年4月23日09:03 | #5

    我以为primitive function都是`C source code’的。
    你的意思是说指定到/usr/share/emacs/23.1.90/lisp/loaddefs.el,就可以在emacs -q后直接用命令了?

    [回复]

    ahei 回复:

    我刚才写错了,你再看看,”primitive function”确实都是c写的,info:

    A “primitive function” is a function callable from Lisp but written in
    the C programming language.

    emacs的loaddefs.el是emacs自己加载的,不需要你指定的阿

    [回复]

    匿名 回复:

    @ahei, 我怎么觉得subr.el特怪异,emacs -q后,里面的函数、变量就可以直接查看,就像已经load了一样。

    [回复]

    ahei 回复:

    @, 对阿,我刚才说了阿,这些是其他机制保证加载它们的,我目前也不清楚,呵呵,以后搞明白了再写文章。

    [回复]

    匿名 回复:

    @ahei, 我是新手,期待……

    [回复]

  6. xilbert
    2010年5月31日10:56 | #6

    确实是好文,看完后我还有个疑惑,
    在.emacs 中如果有:
    (require ‘view)
    (require ‘info)
    (require ‘grep)
    (require ‘color-theme) 难道emacs会去加载对应的文件吗?我一直没有搞明白autoload与require的区别。

    [回复]

    ahei 回复:

    @xilbert, load每次都会加载,require则是已经加载过了就不加载,否则就加载,autoload则是调用的时候再require

    [回复]

    xilbert 回复:

    @ahei, 太精辟了,短短一句话道出本质,谢谢

    [回复]

    wgf4242 回复:

    @xilbert,
    现在回头看这文章.感觉又有收获了.

    [回复]

    ahei 回复:

    @wgf4242, 呵呵,有收获就好

    [回复]

  7. 雪狼湖
    2011年1月26日18:50 | #7

    我增加了matlab mode,在.myemacs中
    但是我发现有些快捷键比如C-h删除一个字符的快捷键没有被映射到matlabmode 中来,原因是matlab mode中定义了C-h这个快捷键,我怎么能把他给覆盖了呢,因为编辑习惯不一致很讨厌,我删除的时候下意识按C-h
    但是却出来帮助的提示很讨厌,但是也没搞清楚怎么用del-define-keys来覆盖这个matlab mode似乎已经覆盖了,只不过C-h又被重新定义了。

    [回复]

    ahei 回复:

    你用define-key在matlab mode map中重新定义一下C-h不就ok了吗?

    [回复]

  8. 2011年12月4日20:28 | #8

    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.

    [回复]

  9. 2012年8月23日14:58 | #9

    Hi,

    ahei老大,你好,这篇文章写得很好,解决了我对于require,eval-after-load一直以来的疑问,但是我有另外一个小疑问,就是:
    假设在file.el里面定义了一个函数test-file,而且把test-file定义成了autoload类型,那是不是就算是我去eval (require ‘file)也不会加载test-file,而是一定要等到我第一次调用test-file时才会去加载?
    期待回复!

    Best Regards,
    Kelvin

    [回复]

  1. 2010年4月26日07:21 | #1
  2. 2010年4月28日06:10 | #2
  3. 2010年11月29日13:52 | #3
  4. 2011年4月3日12:42 | #4
:wink: :-| :-x :twisted: :) 8-O :( :roll: :-P :oops: :-o :mrgreen: :lol: :idea: :-D :evil: :cry: 8) :arrow: :-? :?: :!: