向WindJS致敬_Node异步流程控制4

写在前面

老赵(Jeffrey Zhao)是笔者敬佩的能做20个引体向上的前辈

当时还混博客园的时候有看过几篇老赵的.Net文章,后来因为选择前端学习JS仔细看了yield系列文章,顿时高山仰止(人家那才叫编程呢,笔者这只能算是代码应用

再后来github追随了老赵(目前笔者唯二follow的人),多半因为技术敬佩,另一小半因为老赵的学生资助计划(虽然现在已经像WindJS一样被历史尘封,但应该向有担当且做实事的人致敬)

一.WindJS的基本原理

Wind.js的确是个“轮子”,但绝对不是“重新发明”的轮子。我不会掩饰对Wind.js的“自夸”:Wind.js在JavaScript异步编程领域绝对是一个创新,可谓前无来者。有朋友就评价说“在看到Wind.js之前,真以为这是不可能实现的”,因为Wind.js事实上是用类库的形式“修补”了JavaScript语言,也正是这个原因,才能让JavaScript异步编程体验获得质的飞跃。

我们现在想要解决的是“流程控制”问题,那么说起在JavaScript中进行流程控制,有什么能比JavaScript语言本身更为现成的解决方案呢?在我看来,流程控制并非是一个理应使用类库来解决的问题,它应该是语言的职责。只不过在某些时候,我们无法使用语言来表达逻辑或是控制流程,于是退而求其次地使用“类库”来解决这方面的问题。

以上引自专访Wind.js作者老赵(上):缘由、思路及发展

基本原理从一个例子说起,如果我们希望给O(n)去重添上动画效果,要怎么做?

0.问题重述

输入:一个数组,元素类型为Number|String(清晰起见,简化问题)

输出:一个不含重复元素的数组

去重方法如下:

// O(n)去重
var unique = function(arr) {
    var dir = {};
    var _arr = arr.slice();
    var res = [];

    _arr.forEach(function(item) {
        // id = type + item,避免String键名冲突
        var id = typeof item + item;
        if (!dir[id]) {
            dir[id] = true;
            res.push(item);
        }
    });

    return res;
};
// test
var arr = [1, 2, '1', 2, 3, 23, 1, '5'];
console.log(unique(arr));
// log print:
// [ 1, 2, '1', 3, 23, '5' ]

那么如果现在要给去重的过程添上动画,要怎么做?

  • 先去重并记录过程细节,去重结束后执行动画序列,动画结束后继续其它业务

似乎只有这一种稳妥的选择,但如果问题不是简单的去重,而是其它更复杂的(比如快排甚至退火算法)呢?是不是要设计一个复杂的数据结构,保存过程细节?考虑快排动画,我们希望表达每趟操作的细节(2个指针如何移动,如何比较,如何赋值…),就必须要用一个巨大的对象数组把这些过程保留下来吗?

纯属浪费,因为排序只想得到排序结果,而不是过程细节,我们辛苦记录的过程细节事实上只用了一次就被丢弃了

换个角度,在排序过程中执行动画才是最合理的,在排序过程中可以拿到所有细节,执行动画,然后下一步排序

但问题是动画在做,排序也在做,Step1的动画完成后,排序已经进行到Step5了。嗯,我们需要让JS停下来

1.让JS停下来

使用yield再简单不过了,F12输入function* + yield语法,想怎么暂停就怎么暂停,关于ES6 yield的更多信息,请查看generator(生成器)_ES6笔记2

注意,现在时间是6年前(2010-6),JS才刚刚迎来ES5时代,没有yield可以用,当时大多数人和笔者一样觉得JS是停不下来的(比如循环如何暂停)

确实,循环停不下来,但是递归完全可以暂停

模拟一个异步动画(CSS动画等等),如下:

var asyncAnim = function(str, callback) {
    console.log(str);
    setTimeout(function() {
        // 耗时动画
        //...
        callback();
    }, 100);
};

然后循环改递归:

var uniqueWithAnim = function(arr, callback) {
    var dir = {};
    var _arr = arr.slice();
    var res = [];
    var animLock = false;

    // 用递归代替循环
    var i = 0, len = _arr.length;
    var find = function() {
        if (i === len) {
            return callback(res);
        }

        var item = _arr[i];
        var id = typeof item + item;

        if (!dir[id]) {
            dir[id] = true;
            res.push(item);
            // 动画
            animLock = true;
            asyncAnim('push ' + item, function() {
                // 动画完成回调
                animLock = false;
                find();
            });
        }
        i++;

        if (!animLock) {
            find();
        }
    };

    // 开始递归
    find();
};

测试一下:

// test
uniqueWithAnim(arr, function() {
    console.log('all done');
});
// log print:
// push 1
// (100ms later)push 2
// (100ms later)push 1
// (100ms later)push 3
// (100ms later)push 23
// (100ms later)push 5
// (100ms later)all done

效果是JS被停下来了,我们通过回调把异步逻辑与同步逻辑串起来了,但是需要改写代码,一点都不美

2.模拟yield

我们想要的是一种通用的能让JS停下来的工具模式,所以需要模拟实现yield

var w = function(fn) {
    // $yield
    var $yield = function(result, next) {
        var res = {};
        res.value = result;

        if (typeof next === 'function') {
            res.next = next;
        }

        return res;
    };

    return fn.bind(null, $yield);
};

w是一个wrapper,给fn注入参数,$yield用来建立step链

试用一下:

var oneTwoThree = w(function($yield) {
    return $yield(1, function() {
    return $yield(2, function() {
    return $yield(3);

    });
    });
});
console.log(oneTwoThree());
console.log(oneTwoThree().next());
console.log(oneTwoThree().next().next());
// log print:
// { value: 1, next: [Function] }
// { value: 2, next: [Function] }
// { value: 3 }

这就是一种简单的yield实现方式,看起来也不美

3.WindJS原理

  • 递归代替循环。把逻辑块拆分为step链,延时调用下一个函数,就是所谓的暂停(Sleep

  • “编译”隐藏改写代码细节。Wind帮我们改写源码,所以效果是我们可以以同步形式编写异步代码

Wind不改变用户的思维方式和编码习惯,也不强加一堆API,而是以“隐式”重写源码的方式来实现,所以看起来像是修改了JS语言本身,以不变应万变,不用像async模块一样提供大而全的方案

P.S.现在ES7已经吸纳了这种方案(提供了async&await),这样看来,Wind确实“修改”了JS语言本身

P.S.如果框架都按这种思路来设计,FEers就没有那么多东西需要学了…如果语言本身吸纳各种特性趋于完美,就不需要什么框架了,顶多还需要工程化工具(比如构建工具等等)

二.“编译”源码

“编译”源码是JS终极黑魔法,像一扇黑气缭绕的大门,代表着无限可能

1.Wind编译示例

读取文件触发异常,示例如下:

var fs = require('fs');
var Wind = require('Wind');

// 任务模型捕获异步异常
var Task = Wind.Async.Task;
var Binding = Wind.Async.Binding;

var readFileAsync = Binding.fromStandard(fs.readFile);
var readFile = eval(Wind.compile('async', function() {
    try {
        var file = $await(readFileAsync('./nosuch.file', 'utf-8'));
    } catch (err) {
        console.log('catch error: ' + err);
    }
}));
// 获取任务对象
var task = readFile();
// 启动任务
task.start();

编译结果如下:

(function () {
    var _builder_$0 = Wind.builders["async"];
    return _builder_$0.Start(this,
        _builder_$0.Try(
            _builder_$0.Delay(
            function () {
                return _builder_$0.Bind(readFileAsync("./nosuch.file", "utf-8"), function (file) {
                     return _builder_$0.Normal();
                });
            }),
            function (err) {
                console.log("catch error: " + err);
                return _builder_$0.Normal();
            },
            null
        )
    );
})

Wind帮我们完成了改写,把同步形式的代码改写为异步回调形式

2.模拟捕获异步回调中的异常

Wind的try...catch能够捕获异步回调中的异常,看起来很神秘,其实原理很简单

// 尝试捕获异步回调中的异常
var ex = {};
ex.Try = function(asyncFn, errHandler) {
    asyncFn.call(null, errHandler);
};
// test
var _readFileSync = function(callback) {
    fs.readFile('./nosuch.file', 'utf-8', callback);
};
ex.Try(_readFileSync, function(err) {
    console.log('catch error: ' + err);
});
// log print:
// catch error: Error: ENOENT: no such file or directory, open 'E:\node\learn\async\wind\nosuch.file'

Wind内部与上面的示例类似,只是我们被compile蒙住了双眼,才看不透这层神秘

3.“编译”

我们模拟的ex.Try看起来一点也不像Wind优雅的原生try...catch,没关系,我们也来试试“编译”美容法:

ex.compile = function(fn) {
    var rTry = /\s+try\s*{([^}]+)}/m;
    var rCatch = /\s+catch[^)]+\)\s*{([^}]+)}/m;
    var source = fn.toString();
    // console.log(source);

    // parse try block
    var sourceTry = rTry.exec(source)[1].trim();
    // console.log(sourceTry);
    var sourceTry = sourceTry.replace(/^[^$]*\$await\s*\((.+)\)\s*\)/m, function(match, p1) {
        // console.log(p1);
        return '(function(callback) {\n' + p1 + ', callback);\n});';
    });
    // console.log(sourceTry);
    var asyncFn = eval(sourceTry);

    // parse catch block
    var sourceCatch = rCatch.exec(source)[1].trim();
    // console.log(sourceCatch);
    sourceCatch = '(function(err) {\n' + sourceCatch + '\n});';
    var errHandler = eval(sourceCatch);

    return {
        start: function() {
            ex.Try(asyncFn, errHandler);
        }
    };
};

最后,测试效果:

// test
var t = ex.compile(function() {
    try {
        var file = $await(fs.readFile('./nosuch.file', 'utf-8'));
    } catch (err) {
        console.log('catch error: ' + err);
    }
});
t.start();
// log print:...

形式和效果都完全一样,这就是Wind的try...catch能够捕获异步回调异常的秘密

“编译”完成的工作如下:

var file = $await(fs.readFile('./nosuch.file', 'utf-8'));
-->
var _readFileSync = function(callback) {
    fs.readFile('./nosuch.file', 'utf-8', callback);
};

console.log('catch error: ' + err);
-->
function(err) {
    console.log('catch error: ' + err);
}

说白了就是字符串拼接,仅此而已

同样的,Wind能处理无限序列就没什么好奇怪的了,示例如下:

// infinite fib series
var fib = eval(Wind.compile("async", function () {

    $await(Wind.Async.sleep(1000));
    console.log(0);

    $await(Wind.Async.sleep(1000));
    console.log(1);

    var a = 0, current = 1;
    while (true) {
        var b = a;
        a = current;
        current = a + b;

        $await(Wind.Async.sleep(1000));
        console.log(current);
    }
}));

fib().start();

while(true)死循环被替换成了含断点的死递归,就像ES6的yield

// infinite fib series in es6 yield
var fib = (function* () {
    yield 0;
    yield 1;

    var a = 0, current = 1;
    while (true) {
        var b = a;
        a = current;
        current = a + b;
        yield current;
    }
})();

fib.next(); // 0
fib.next(); // 1
fib.next(); // 1

三.总结

Wind的特点如下:

  • 编译源码

  • 任务模型

  • 以同步形式编写异步代码

适用场景:适用于从已有的以同步方式编写的代码向Node迁移,能够省去重写代码的开销

那么,ES7的async&await能否取代Wind?

可以,因为实现原理与目标都是一致的

  • 实现原理:yield暂停

  • 目标:以同步形式编写异步代码

ES7的async&await从promise,generator一路辗转走来,而Wind早在5年前就看到了这一天,并提前实现了愿景

学习技术思想本身,而不是单纯的代码应用,这才是编程

参考资料

  • 《深入浅出NodeJS》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code