延续传递样式与管道有什么不同吗?



我一直在学习延续传递风格,特别是在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"

reqres 分别是 HTTP 请求和 HTTP 响应的命名约定。

Express.JS 中的路由处理程序将由一个或多个中间件函数组成。Express.js 将向它们中的每一个传递reqres对象,其中包含对下一个中间件所做的更改,以及相同的next回调。

app.get('/get', middlware1, middlware2, /*...*/ , middlewareN, customErrorHandler)

next回调函数用于:

  1. 作为中间件的延续

    • 调用 next() 会将执行流传递给下一个中间件函数。在这种情况下,它履行了其作为延续的作用。
  2. 也可作为路由拦截器

    • 调用 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'

希望这有帮助!

最新更新