# 第八章 VimL 异步编程特性 ## 8.1 异步工作简介 异步机制是 vim8 版本引入的新机制,准确地说,是从 7.4 某个补丁开始引入,不过在 vim8 完善并正式发布。这一全新特性使得 vim 直接跳升一大版本号,可见意义非凡。 ### 8.1.1 同步工作可能的问题 要理解异步的特性,不妨先回顾下在此之前只能同步工作的情况,会遭遇哪些不便。 比如要从一个目录下的文本文件中查找某个字符串,我们知道(在 unix 系统中)直接 有个 `grep` 工具可用。而在运行着的 vim 中,也可以通过 `:!grep ...` 命令调用系 统的 `grep` 工具。但是用 `:!` 执行外部命令的话,会临时切回启动当前 vim 的终端 ,外部命令的输出在该终端上;当外部命令经过或长或短的时间完成后,还需要等用户按 回车确认才回到 vim 正常的用户界面。如果是 windows 系统的 gVim ,`:!` 执行外部 命令则会弹出 `cmd` 黑框,展示外部命令的输出,也需要由用户确认关闭该黑窗才能回 到 gVim 编辑窗口。 显然按种方式,在运行外部命令的同时,在回到 vim 界面之前,vim 对用户而言是停止 工作的,比如用户暂时无法操纵 vim 进行编辑工作。vim 也有个类似的内部命令 `:vimgrep` 用于在多文件中搜索字符串,并将结果输出在 `quickfix` 窗口。运行该命 令不会切回 shell 终端,与 `:!grep` 很有些不同。但是,如果待搜索的文件很多,尤 其是类似 `**/*` 的递归所有子目录的文件搜索时,`:vimgrep` 命令完成搜索也可能很 慢,需要等待一段时间才能完成搜索。在等待的这段时间内,虽然仍然停留在 vim 界面 ,但 vim 也好像停止了与用户的交互工作,譬如按 `j` `k` 不见得会移动光标。事实上 vim 还是监测到你按了 `j` `k` 键,只不过要等 `:vimgrep` 这个慢命令完成后才会响 应后续按键。简单地说,就可能造成明显卡顿。 这就是旧版本 vim 按同步工作方式可能出现的问题。你可以将 vim 编辑器想象为一个单 线程的无限循环程序,等待着用户的按键,并立即根据按键命令处理工作。正常情况下 vim 响应用户按键命令是极快的,所以用户感觉很流畅。因为正常人类的击键速度在计算 机程序看来都太慢了,vim 在大部分时间里都是在等待用户击键的。但是当用户试图让 vim 执行某些“能感觉出来慢”的命令时,问题就浮现了,影响用户体验。 如果上面的 `:vimgrep` 命令没让你感觉到慢,可以用 VimL 定义如下的慢函数: ```vim function! SlowWork() sleep 5 echo "done!!" endfunction ``` 然后在命令行输入 `:call SlowWork()` 并回车,你应该就能感觉到 vim 明显卡顿了。 在此期间若按几次 `j` ,也要等该函数返回才能发现光标移动。此外,你也可以试试用 `while 1 | endwhile` 定义一个无限循环函数,调用时会令 vim 完全停止响应,如此 请用 `Ctrl-C` 强制结束当前命令,回到 vim 的正常工作状态来。 ### 8.1.2 异步工作想解决的问题 显然,vim8 引入的异步机制,就是试图解决(或部分解决、缓和)上述同步模型中出现 的“慢命令卡顿”问题。当然它也不是直接重定义优化原来命令的工作方式,因为兼容旧习 惯也是 vim 的传统。所以,在 vim8 中,类似 `:!grep` 或 `:vimgrep` 命令,该怎么 慢还怎么慢,它真正想优化的类似 `system('grep')` 函数的工作方式。 `system('grep')` 与 `:!grep` 的相同之处在于都是调用外部命令(系统可执行程序) ,只不过调用 `system()` 函数不会切到 shell 终端,仍停留在 vim 界面。所调用的外 部命令的输出会被 `system()` 函数所捕获,可以保存在 VimL 变量中,供脚本后续使用 。如果该外部命令执行时间较长, vim 用户仍会感到停止响应或卡顿。 然后在 vim8 中,就提供了另一套不叫 `system()` 名字的函数,用于执行外部命令。 vim 不再等待外部命令结束,而是立即返回给用户,可以立即接着响应用户按键。等外部 命令终于结束了,vim 再调用一个回调函数处理结果。 开启异步工作的具体函数与用法,留待下一节详细介绍。不过你应该能感觉与估计到,这个 异步编程模型比本书之前介绍的同步编程模型要复杂些。并且在监测外部命令结束时准备 回调也必然有其他开销,所以异步也不宜滥用,只适合在(可预期)比较耗时的外部命令 上。如果只是简单的可以快速完成的外部命令,仍用原来的 `system()` 函数完成工作即 可。 另外要提及的是,目前 vim8 版本的异步机制,也只能将外部命令以异步的方式开启,并 不能用异步的方式执行内部命令。也就是说,不论是 vim 内置的命令(及常规函数), 还是用 VimL 写的自定义命令(函数),都仍只能按原来的同步方式执行,暂无异步用法 。 ### 8.1.3 异步机制带来的 vim 新特性简介 vim8 提供异步机制后,可以据此实现很多新特性。比如内置终端(从 vim8.1 版本开始 支持)。在命令行执行 `:terminal` 就能打开一个新窗口,体验一下内置终端。在这个 特殊的 vim 新窗口中,就相当于运行着一个 shell ,可以像系统 shell 一样执行任何 命令,甚至也可以在此又运行一个 vim (不过一般情况下不建议这么玩)。用窗口切换 快捷键 `Ctrl-w` 可以回到之前的普通 vim 窗口,正常操作 vim 进行编辑工作。 也就是说,内嵌终端正是异步运行的,并不中断 vim 本来的编辑工作。相比在这个功能 出现之前,用 `:shell` 命令打开的子终端,就会切出 vim 界面,只能在那个子终端中 工作,必须在那执行 `$ exit` 退出子终端,才能回到 vim 。 关于内置终端的详细用法请参考 `:help terminal` ,在那文档中还介绍了在 vim 中“ 嵌入”gdb 调试 vim 本身的示例。表明内置终端功能其实不止能执行一个 `shell` ,还 适于执行其他任何交互程序,例如 python 解释器,mysql 客户端,gdb 调试器等。 不过本章不是介绍 vim 的这类新特性,而是侧重介绍 VimL 脚本编程中如何使用这个异 步机制,据此可以完成之前的脚本无法完成的工作,或优化某些插件功能。 ### 8.1.4 异步编程的简单运用:定时器 让我们先看一个简单的例子来体验下异步编程的风格,定时器(请确认 vim 编译包含 `+timers` 特性)。将上文按传统同步风格定义的 `SlowWork()` 函数重新改写如下: ```vim function! SlowWork() call timer_start(5*1000, 'DoneWork') endfunction function! DoneWork(timer) echo "done!!" endfunction call SlowWork() ``` 现在再调用 `SlowWork()` 函数时就不会“暂停” 5 秒了,该调用立即返回,用户可如常 操作 vim 。大约过了 5 秒后,函数 `DoneWork()` 被调用,显示 `"done!!"` 。 这里的关键是在 `SlowWork()` 中用 `timer_start()` 启用了一个定时器。参数一是时 间,单位毫秒;参数二叫回调函数,应该是函数引用,但也可用函数名代替。其意义就是 在指定时间后调用那个回调函数,而不影响现在 vim 对用户的正常响应。还可以指定可 选的第三参数,表示重复回调若干次,默认就只回调一次,然后自动关闭定时器。该函数 有返回值,表示定时器 ID,在 vim 内部就用该 ID 标记这个定时器。回调函数一般是自 定义函数,必须接收一个表示定时器 ID 的参数。不过在这个简单示例中,我们忽略未用 到这个定时器 ID 参数。定时器相关函数的详细用法请参考 `:help timer-functions` 。 由此可见,异步编程的基本思路是将原来在一个函数内的工作(一般是较费时的工作), 多拆出一个回调函数,用来在工作完成时处理“后事”,关键也就是回调函数的编写。在这 个例子中,我们用定时器来“模拟”了一件慢工作,当然定时器本身也另有用途场景。 定时器可以明确指定延时几秒,不过在实际的慢工作(外部命令)中,需要多长时间完成 工作是不确定的。这就需要另外的机制,根据其他条件来调用回调函数。这就是下一节准 备讲的“任务”,原文档术语叫 `job` 的话题了。