我一直在学习延续传递风格,特别是在javascript中实现的异步版本,其中函数将另一个函数作为最终参数并创建对它的异步调用,将返回值传递给第二个函数。
但是,我不太明白继续传递除了重新创建管道(如在 unix 命令行管道中)或流之外还有什么作用:
replace('somestring','somepattern', filter(str, console.log));
与
echo 'somestring' | replace 'somepattern' | filter | console.log
除了管道干净得多。使用管道,似乎很明显,数据被传递,同时执行被传递给接收程序。事实上,对于管道,我希望数据流能够继续沿着管道传递,而在 CPS 中,我希望有一个串行过程。
也许可以想象,如果通信对象和更新方法与数据一起传递,而不是完全切换和返回,则CPS可以扩展到连续管道。
我错过了什么吗?CPS在某些重要方面是否不同(更好?
需要明确的是,我的意思是延续传递,其中一个函数将执行传递给另一个函数,而不仅仅是普通回调。CPS 似乎意味着将一个函数的返回值传递给另一个函数,然后退出。
UNIX pipes vs async JavaScript
unix 管道的行为方式与您链接到的异步 CPS 代码之间存在很大的根本差异。
主要是管道阻止执行,直到整个链完成,而您的异步 CPS 示例将在进行第一次异步调用后立即返回,并且仅在完成后执行您的回调。(在您的示例中,超时等待完成时。
看看这个例子。我将使用 Fetch API 和 Promise 来演示异步行为,而不是 setTimeout 以使其更加逼真。假设第一个函数f1()
负责调用某个 Web 服务并将结果解析为 json。这被"管道"到处理结果的f2()
中。
CPS风格:
function f2(json){
//do some parsing
}
function f1(param, next) {
return fetch(param).then(response => response.json()).then(json => next(json));
}
// you call it like this:
f1("https://service.url", f2);
如果将调用从 f1 中移出 f2,则可以编写语法上看起来像管道的内容,但这与上述完全相同:
function f1(param) {
return fetch(param).then(response => response.json());
}
// you call it like this:
f1("https://service.url").then(f2);
但这仍然不会阻止。你不能在javascript中使用阻塞机制来完成这个任务,根本没有机制可以阻止Promise。(在这种情况下,您可以使用同步 XMLHttpRequest,但这不是这里的重点。
CPS 与管道
上述两种方法之间的区别在于,谁有权决定是否调用下一步,以及确切地使用什么参数,调用方(后面的示例)或被调用函数(CPS)。
CPS非常方便的一个很好的例子是中间件。例如,考虑处理管道中的缓存中间件。简化示例:
function cachingMiddleware(request, next){
if(someCache.containsKey(request.url)){
return someCache[request.url];
}
return next(request);
}
中间件执行一些逻辑,检查缓存是否仍然有效:
如果不是,则调用
next
,然后将继续处理管道。如果它有效,则返回缓存的值,跳过下一次执行。
应用程序级别的延续传递样式
在应用程序级别分解延续传递样式可以通过其"延续"函数(也称为回调函数)提供流控制优势的途径,而不是在表达式/功能块级别进行比较。让我们以Express为例.js:
每个快速中间件都采用相当相似的 CPS 函数签名:
const middleware = (req, res, next) => {
/* middleware's logic */
next();
}
const customErrorHandler = (error, req, res, next) => {
/* custom error handling logic*/
};
next
是 Express 的本机回调函数。
更正:next() 函数不是 Node.js 或 Express API 的一部分,而是传递给中间件函数的第三个参数。next() 函数可以命名为任何名称,但按照惯例,它总是被命名为"next"
req
和 res
分别是 HTTP 请求和 HTTP 响应的命名约定。
Express.JS 中的路由处理程序将由一个或多个中间件函数组成。Express.js 将向它们中的每一个传递req
、res
对象,其中包含对下一个中间件所做的更改,以及相同的next
回调。
app.get('/get', middlware1, middlware2, /*...*/ , middlewareN, customErrorHandler)
next
回调函数用于:
作为中间件的延续:
- 调用
next()
会将执行流传递给下一个中间件函数。在这种情况下,它履行了其作为延续的作用。
- 调用
也可作为路由拦截器:
- 调用
next('Custom error message')
会绕过所有后续中间件,并将执行控制传递给customErrorHandler
进行错误处理。这使得在路线中间"取消"成为可能! - 调用
next('route')
会绕过后续中间件,并将控制权传递给下一个匹配路由,例如/get/part。
- 调用
在JS中模仿管道
有一个关于管道的TC39建议,但在它被接受之前,我们必须手动模仿管道的行为。嵌套 CPS 函数可能会导致回调地狱,因此这是我尝试使用更干净的代码:
假设我们想通过替换起始字符串的一部分来计算句子"狐狸跳过月球"(例如props
)
const props = " The [ANIMAL] [ACTION] over the [OBJECT] "
每个替换字符串不同部分的函数都用数组排序
const insertFox = s => s.replace(/[ANIMAL]/g, 'fox')
const insertJump = s => s.replace(/[ACTION]/g, 'jumps')
const insertMoon = s => s.replace(/[OBJECT]/g, 'moon')
const trim = s => s.trim()
const modifiers = [insertFox, insertJump, insertMoon, trim]
我们可以实现同步的、非流的、管道行为与reduce
。
const pipeJS = (chain, callBack) => seed =>
callBack(chain.reduce((acc, next) => next(acc), seed))
const callback = o => console.log(o)
pipeJS(modifiers, callback)(props) //-> 'The fox jumps over the moon'
这是pipeJS
的异步版本;
const pipeJSAsync = chain => async seed =>
await chain.reduce((acc, next) => next(acc), seed)
const callbackAsync = o => console.log(o)
pipeJSAsync(modifiers)(props).then(callbackAsync) //-> 'The fox jumps over the moon'
希望这有帮助!