17

我一直在学习继续传递样式,特别是在 javascript 中实现的异步版本,其中一个函数将另一个函数作为最终参数并创建对其的异步调用,将返回值传递给第二个函数。

但是,我不太明白延续传递除了重新创建管道(如在 unix 命令行管道中)或流之外还有什么作用:

replace('somestring','somepattern', filter(str, console.log));

对比

echo 'somestring' | replace 'somepattern' | filter | console.log

除了管道要干净得多。使用管道,很明显数据被传递,同时执行被传递给接收程序。事实上,对于管道,我希望数据流能够继续通过管道,而在 CPS 中,我希望是一个串行过程。

可以想象,如果通信对象和更新方法与数据一起传递,而不是完整的切换和返回,CPS 可以扩展到连续管道。

我错过了什么吗?CPS 是否在某些重要方面有所不同(更好?)?

明确地说,我的意思是继续传递,其中一个函数将执行传递给另一个函数,而不仅仅是简单的回调。CPS 似乎暗示将一个函数的返回值传递给另一个函数,然后退出。

4

2 回答 2

9

UNIX 管道与异步 javascript

unix 管道的行为方式与您链接到的异步 CPS 代码之间存在很大的根本区别。

主要是管道阻塞执行,直到整个链完成,而您的异步 CPS 示例将在第一次异步调用后立即返回,并且仅在完成时执行您的回调。(在您的示例中,当超时等待完成时。)

看看这个例子。我将使用Fetch API和 Promises 来演示异步行为,而不是使用 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);

如果将 f2 的调用移出 f1,则可以编写语法上看起来像管道的东西,但这将与上面完全相同:

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调用 then ,然后继续处理管道。

  • 如果有效,则返回缓存值,跳过下一次执行。

于 2018-11-07T12:33:22.707 回答
4

应用程序级别的继续传递风格

不是在表达式/功能块级别进行比较,而是在应用程序级别考虑延续传递样式可以通过其“延续”函数(也称为回调函数)提供流控制优势的途径。让我们以Express.js为例:

每个express 中间件都采用非常相似的 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”</p>

req并且res分别是 HTTP 请求和 HTTP 响应的命名约定。

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

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

next回调函数用于:

  1. 作为中间件的延续

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

    • 调用next('Custom error message') 绕过所有后续中间件,并将执行控制传递customErrorHandler给错误处理。这使得在路线中间“取消”成为可能!
    • 调用next('route')绕过后续的中间件并将控制权传递给下一个匹配的路由,例如。/获取/部分。

在 JS 中模仿管道

pipe有一个 TC39 提案,但在它被接受之前,我们必须手动模仿 pipe 的行为。嵌套 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'

希望这可以帮助!

于 2018-11-13T18:35:54.440 回答