写在前面
老赵(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语言本身更为现成的解决方案呢?在我看来,流程控制并非是一个理应使用类库来解决的问题,它应该是语言的职责。只不过在某些时候,我们无法使用语言来表达逻辑或是控制流程,于是退而求其次地使用“类库”来解决这方面的问题。
基本原理从一个例子说起,如果我们希望给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》