8.3 使用通道控制任务 8.3.1 通道的概念
Vim 手册中的术语直译为通道,听起来比任务工作更抽象。 上一节介绍的任务,如果直观地想一想,即使它不是一个可以瞬间完成的“慢速”命令,它也是一个“短暂”的命令。 你可以期待它完成,任务就完成了。
显然,我们可以使用()同时启动多个异步命令,但如果我们试图通过这种方式启动一组看似相关的任务,则可能无法达到我们的目的。 因为启动的不同任务是相互独立的,在后台独立运行。 例如,连续打开以下两条命令:
: call job_start('cd ~/.vim/')
: call job_start('ls')
这两条语句写在一起无法(也许有些人认为这是理所当然的)进入目标目录并列出文件。 第一条语句启动后台命令 cd 进入目录,但不执行任何操作就完成了; 第二条语句启动另一个独立的后台命令 ls 并仍然列出当前目录中的文件。
不过,vim 中有一个解决这个需求的方法。 想想vim8.1中添加了异步功能的内置终端功能。 显然,您可以打开内置终端并在内置终端中输入 cd ls 命令来列出目标。 目录中的所有文件:
: terminal
$ cd ~/.vim
$ ls
既然你可以在vim中列出一串内容,那么你可以想象还有其他方法可以将列出的内容捕获到VimL变量中,然后执行所需的程序逻辑处理。
:该命令实际上有一个默认参数,即异步启动一个交互式shell进程(如bash)。 不过,这个任务与上一节介绍的异步任务不同。 特别的是,它不会自动结束,相当于一个无限循环等待用户输入,然后解释并执行(shell命令)并给出响应。 那么一定有东西连接到vim与后台异步启动的shell进程(任务),以方便相互通信。 这个东西就是所谓的“通道”。
通道的一端自然连接到vim,另一端通常连接到可以长时间运行的服务程序。 上一节介绍的异步任务也与外部命令有联系,这样vim就可以知道外部命令什么时候有输出、什么时候结束,并可以在适当的时候调用回调函数。 只是外部命令自然结束后,通道就被切断了。 所以最好反过来理解。 通道是底层更通用的机制,任务是短、平、快的特殊通道。
Vim 的在线文档:help 有专门的文档来描述通道(和任务)的使用细节,并且一开始还有一个在 user 中编写的简单的服务程序,用于演示 vim 的通道连接和交互。 对于熟悉它的读者,您可以按照这个演示示例进行操作。 从这样简单朴素的服务开始,渠道可以实现内置终端等复杂且标志性的功能。 虽然我们在学习VimL的时候并不指望能够一口气写出这么复杂的高级函数,但是了解通道的机制,掌握通道的使用,可以极大的拓展VimL编程的性能,满足无法满足的需求。在旧版本中可以实现。
8.3.2 打开通道和模式选项
要打开通道,请使用 () 函数。 我们来对比一下它的函数“原型”和前两节介绍的定时器和任务启动函数:
定时器的第一个参数是时间,因为它会在一定时间内执行工作。 同时,要使定时器生效,必须在第二个参数处提供回调函数,以指示届时将执行特定操作。 对于任务来说,不可能提前知道执行外部命令需要多少时间(毫秒)。 因此,启动任务的第一个参数是外部命令。 有时候这就足够了,只要在后台默默地完成即可; 后续选项是可选的,对于复杂的任务,可能需要在不同时间进行多次回调。 ,因此它们都被封装在一个更大的选项字典中,使得使用界面变得简单明了。
至于通道,则更为抽象,因为它实际上并不是针对某个特定的命令,而是针对某个“地址”,就像编程领域中“主机:端口”的地址概念一样。 Vim 的通道可以连接到这样的地址并与另一端的服务进行通信。 至于用什么语言编写的命令和程序,另一端的服务不需要关心,也不影响。
在通道选项集中,除了同样重要的回调函数之外,还有一个更基本的模式选项需要关注,称为模式。 mode 指定 Vim 与另一端程序通信时的消息格式。 粗略地讲,可以直观地理解为传输、读写的字符串格式。 总共支持四种模式。 上一节介绍的 () 启动的任务默认使用 NL 模式,即换行符分隔每条消息(字符串)。 这里使用()打开的通道默认使用json格式。 json是目前网上非常流行的格式,vim现在也内置了json的解析,所以使用起来简单灵活。
另外两种模式称为js和raw。 js模式是类似于json的一种样式格式,文档上说比json效率更高。 因为js编码解码中没有那么多双引号,空值可以省略。 Raw表示原始格式,即没有特殊格式。 Vim 无法对此做出任何假设或预处理,必须由用户在回调函数中处理。
至于在具体的 VimL 编程实践中使用哪种通道模式,取决于连接另一端的程序如何提供服务。 最好能提供json或者js,不然NL模式简单,而且如果不一定能保证换行,那就只能用raw了。 如果另一端的程序也是你自己开发的,你就会有更大的控制权。 如果是简单的,可以使用NL模式,但是对于复杂的服务,建议使用json。
该模式之所以重要,是因为它深刻地影响了回调函数的编写方式。 例如,vim 每次从通道接收消息时,都会调用该选项指定的函数(引用),并向其传递两个参数; 因此,回调函数通常如下所示:
function! Callback_Handler(channel, msg)
echo 'Received: ' . a:msg
endfunction
第一个参数a:是通道ID,是()的返回值,代表一个特定的通道(显然可以同时运行多个通道)。 第二个参数a:msg所谓的消息与通道模式有关。 如果是 json 或 js 模式,虽然 vim 接收到的消息最初是字符串,但 vim 会自动帮你解码,因此 a:msg 会转换为 VimL 数据类型,例如丰富嵌套的字典和列表结构。 如果是NL模式,则是去掉换行符的字符串; 当然,如果是raw模式,就是原始消息,回调中可能会有换行,用户一定要注意。
8.3.3 渠道互动
与任务不同的是,仅通过 () 打开通道是不够的。 这只是建立连接并告诉您已准备好使用另一端的程序服务。 但一般它不会自动做具体的工作。 你需要让vim与另一端的服务通信,告诉对方我要做什么,请对方帮忙完成,并等待响应(异步或同步)。 虽然有些服务可以主动发送一些消息给vim,让vim自动处理,但毕竟是有限的。 你不能让外部程序在没有启动控制的情况下影响vim,对吗? 所以消息的来回传递就是通道的正常运作,这也是它强大的功能所在。
消息的交互方式也与通道模式有关。
以json或js方式向通道(另一端)发送消息,推荐使用以下三种方式之一:
call (, {expr})call (, {expr}, {'': })let = (, {expr})
注意前两种写法。 使用:call命令直接调用函数并忽略函数返回值。 它只是异步发送消息并等待响应; 当稍后收到响应时,将调用通道的回调函数。 然而,与第二种用法一样,发送消息时提供了额外的选项,并且单独指定了该消息的回调函数。
所以需要一种机制来区分是哪条消息。 vim 发送消息时,实际上是发送 [{},{expr}],即在消息前附加一个数字,形成一个二进制列表。 该数字由 vim 内部处理,并且通常会递增以确保唯一性。 {expr} 是程序员指定的有效 VimL 值(或数据结构),然后由 vim 编码成 json 字符串,或者类似 js 风格的字符串。 。 通道的另一端收到这样的消息,解码出json字符串,经过内部处理后,通过通道发送回vim,也是一个由数字和消息体组成的二进制列表[{},{}] 。 在同一个请求-响应中,数量是相同的,vim可以相应地将其分配给相应的回调函数。 传入的第二个参数是{},不包含编号的消息体。 当然,如果按照第一种写法发送消息时没有指定回调,那么当收到响应时,会默认分配给()中指定的回调函数。
至于第三种写法,一般使用:let命令来获取()的返回值。 这是同步等待,就像 () 函数捕获输出一样。 虽然同步可能会阻塞,但优点是程序逻辑简单,不用担心回调函数。 当通道建立后,如果另一端的服务程序也在本机上运行,则()可能比()更快。 因此,如果预计请求的操作不是太复杂,可以尝试使用这种同步消息来组织编程。 另外,通道还具有超时选项,这将防止vim陷入无限等待的糟糕情况。 如果超时或错误,() 返回空字符,否则返回解码后的 VimL 数据,就像 () 收到响应时传递给回调函数的消息正文一样。
对于NL或raw模式,不能使用上述两个函数进行交互,而应该使用另外两个相应的函数:
call (, {})call (, {}, {'': ''})let = (, {})
第二个参数必须是字符串,而不是其他复杂的 VimL 数据结构,并且您可能需要手动添加尾随换行符(取决于通道另一端程序的需要)。
json和js模式的通道也可以使用()和(),但是需要提前调用()将要发送的VimL数据转换(编码)成json字符串,然后传递给这两个函数; 那么当收到响应时,您需要使用 () 解码响应消息以获得方便使用的 VimL 数据。
所以,所谓通道的四种模式,是指通道的vim端如何处理消息,以及vim能够自动处理消息的程度。 至于消息在通道的另一端如何处理,那是vim无法控制的。 这是一个编程主题。 也许那里的程序也有一个网络框架,可以自动解码json并将其转换为目标语言的内部数据,或者你需要手动调用json库的相关函数,或者简单地自己解析json字符串......仅此而已和vim一样。 边缘与此无关。 他们只是达成一致,需要传输一个双方都能正确解析的字符串(消息字节)。
另外,还要区分另一个概念。 通道的四种分析模式与通道的两种通信模式不是一个级别的。 后者指的是或pipe,是与操作系统进行进程间通信的底层概念,而前者的json或NL是VimL应用程序级模式。 上一节介绍的任务由()启动,并使用管道来重定向标准输入、输出和错误; 本节介绍的通道以 () 开头,并绑定到特定的端口地址。 。 那么,在vim中,任务管道也被视为一种特殊的通道。
8.4.4 通道示例:自制简单 vim-
本节最后打算介绍一个网友写的伪终端插件:
这应该是因为vim8.1中还没有推出内置终端,而是先提供了+job和+编写的插件,目的是直接在vim中模拟终端,执行shell命令。 虽然不如后来的vim内置终端强大,但还是有自己的特点。 关键是它比较轻量,代码不多。 你可以用它来学习如何使用vim任务和通道的异步功能。 学习和阅读源代码也是学习任何语言编程的绝佳方法。
首先你应该明白,作为一个发布在网络上的插件,或多或少都会追求一定的通用性,所以插件中不可避免地会涉及到很多配置,比如全局变量的判断和设置等。 就像这个插件一样,它想同时用于vim和nvim。 两者为异步函数提供的内置函数接口可能略有不同。 不过,它也想兼容vim7。 如果低版本中没有异步函数,则回退到使用 () 代替。
抛开这些“吵闹”的信息,直接进入关键代码,看看如何使用vim的异步功能。 从功能描述开始,主要提供:命令。 找到源码中的命令定义,了解其调用的私有函数s::
command! -nargs=* -complete=file ZFTerminal :call s:zfterminal()
function! s:zfterminal(...)
let arg = get(a:, 1, '')
" ... (省略)
let needSend=!empty(arg)
if exists('b:job')
let needSend=1
else
call s:updateConfig()
let job = s:job_start(s:shell)
let handle = s:job_getchannel(job)
call s:initialize()
let b:job = job
let b:handle = handle
if exists('g:ZFVimTerminal_onStart') && g:ZFVimTerminal_onStart!=''
execute 'g:ZFVimTerminal_onStart(' . b:job . ', ' . b:handle . ')'
endif
endif
if needSend
silent! call s:ch_sendraw(b:handle, arg . "\n")
endif
" ... (省略)
endfunction
这里的想法是将打开的任务保存在 b:job 中。 这是必要的,因为后续回调函数将使用任务 ID(函数通道 ID)。 不能保存在函数内的局部变量中,否则无法在函数作用域外引用ID,也不宜污染全局变量。 因此,脚本级的s:变量是合适的; 如果异步任务总是与某个任务关联,则存储在b:范围内,不太清晰,可以轻松支持多个任务并行。 它用作shell前端,因此保存为b:job。
如果执行命令时任务不存在,则使用()启动任务,否则使用()向与任务关联的通道发送消息。 它为这两个函数做了一个浅层包装(主要是出于兼容性代码考虑和定义一些默认选项)。 ()开启是这样的:
function! s:job_start(command)
" ...
return job_start(a:command, {
\ 'exit_cb' : 'ZFVimTerminal#exitcb',
\ 'out_cb' : 'ZFVimTerminal#outcb_vim',
\ 'err_cb' : 'ZFVimTerminal#outcb_vim',
\ 'stoponexit' : 'kill',
\ 'mode': 'raw',
\ })
endfunction
这里指定了几个回调函数,并将通道模式设置为raw。 所以在下面的:命令中,使用()来发送消息。 请注意,发送消息需要通道 ID 参数。 使用()函数获取任务关联的通道,它也保存在b:作用域中。 至于回调函数,请根据实现的功能自行跟踪,这里不再赘述。