CPS变换与JavaScript流程控制

正如上一篇文章所说,利用CPS原理可以进行流程控制。那么在JavaScript编程中究竟如何消灭callback hell呢?

co 正是一个解决JavaScript异步流程难题的库。它利用ES2015标准中的 yield,实现了与(本来被期望加入ES2016标准却因为大厂跳票没赶上deadline所以只能明年再说的)async/await 相似的功能而红透半边天。其原理就是利用 yield 的状态机特性,将(看起来是)同步的代码转换为CPS的形式。

co的源文件有两百多行,而其核心思想其实非常简洁——不断的将 yieldnext 值追加到 promise 链中,达到“一步接一步”执行的效果。

这个核心思想用coffeescript写出来不到十行:

fuck = (gen) ->
    it = do gen
    go = (res) =>
        p = it.next(res)
        if p.done
            return
        p.value.then go
    do go

使用起来很简单,在异步语句前面都加上 yield 即可。比如下面的代码很简单的实现了每一毫秒按顺序 log 100个数

let shit = i =>
    new Promise((res, rej) => setTimeout(() => res(i), 1))

fuck(function *() {
    for (let i = 0; i < 100; i++) {
        let holy = yield shit(i)
        console.log(holy)
    }
    console.log("done")
})

运行结果

(运行结果,so beautiful)

看出来了吧?其实它的效果就是把每个 yield 断开之后的代码都写到 Promise::then 里,其实就相当于

~function () {
    // 第一次next()
    var p = shit(0).then(holy => console.log(holy))
    for (let i = 1; i < 100; i++) {
        p.then(shit(i).then(holy => console.log(holy)))
    }
    p.then(holy => console.log("done!"))
}()

由于 Promise 的修饰,这段代码看起来不是那么的CPS。如果能把 Promise 拿掉就好了。如何拿掉 Promise 呢?

从形式上来看,每次添加单个 thenPromisecallback 调用长得差不多,只是把表示 continuation 的参数移到了 then

doSomeThingWithPromise().then(callback)

doSomeThingWithCb(callback)

……那么即使不进行深层次考虑,也很容易实现从 Promisecallback 的转化:

fuck = (gen) ->
    it = do gen
    go = (res) =>
        p = it.next res
        if p.done
            return
        p.value go
    do go

不就是拿掉一个 then 嘛!

用起来也就是去掉了Promise

let shit = i => callback =>
    setTimeout(() => callback(i), 1)

fuck(function *() {
    for (let i = 0; i < 100; i++) {
        let holy = yield shit(i)
        console.log(holy)
    }
    console.log("done")
})

它的等价调用便是典型的Continuation Passing Style
(其实Promise 本身便是一种CPS变换)

~function () {
    let p = (next) => next()
    for (let i = 0; i < 100; i++) {
        let c = ((p) => // 此处的闭包是考虑到js的执行策略,避免陷入无限递归
            (next) => p(() => 
                shit(i)((holy) => {
                    console.log(holy)
                    next()
                })
            ))(p)
        p = c
    }
    p(holy => console.log("done!"))
}()

So beautiful! tj 真是个天才!