Preface
Old Zhao (Jeffrey Zhao) is a senior I admire who can do 20 pull-ups
When I was mixing in Cnblogs back then, I read several of Old Zhao's .Net articles, later because I chose frontend to learn JS I carefully read the yield series articles,顿时 felt like looking up at a high mountain (that's what you call programming, what I do can only be called code application)
-
Fun Programming: Implementing simple yield functionality in JavaScript (Problem)
-
Fun Programming: Implementing simple yield functionality in JavaScript (1 - yield and yieldSeq)
Later I followed Old Zhao on github (currently one of only two people I follow), mostly because of technical admiration, another small part because of Old Zhao's Student Sponsorship Program (although now like WindJS it has been buried by history, but should pay tribute to people who take responsibility and do real things)
I. Basic Principles of WindJS
Wind.js is indeed a "wheel", but absolutely not a "reinvented" wheel. I won't hide my "boasting" about Wind.js: Wind.js is absolutely an innovation in the JavaScript asynchronous programming field,可谓 unprecedented. A friend commented that "before seeing Wind.js, I really thought this was impossible to implement", because Wind.js actually "patched" the JavaScript language in the form of a library, and it's precisely for this reason that JavaScript asynchronous programming experience can achieve a qualitative leap.
What we want to solve now is the "flow control" problem, so speaking of flow control in JavaScript, what could be more ready-made solution than JavaScript language itself? In my opinion, flow control is not a problem that should be solved using libraries, it should be the responsibility of the language. It's just that sometimes we cannot use the language to express logic or control flow, so we settle for using "libraries" to solve problems in this aspect.
Above quoted from Interview with Wind.js author Old Zhao (Part 1): Origins, Ideas and Development
Basic principles start with an example, if we want to add animation effects to O(n) deduplication, how to do it?
0. Problem Restatement
Input: An array, element type is Number|String (for clarity, simplify problem)
Output: An array without duplicate elements
Deduplication method is as follows:
// O(n) deduplication
var unique = function(arr) {
var dir = {};
var _arr = arr.slice();
var res = [];
_arr.forEach(function(item) {
// id = type + item, avoid String key name conflict
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' ]
So if now we want to add animation to the deduplication process, how to do it?
- First deduplicate and record process details, execute animation sequence after deduplication ends, continue other business after animation ends
Seems like only this one safe choice, but if the problem is not simple deduplication, but other more complex ones (such as quicksort or even annealing algorithm)? Do we need to design a complex data structure to save process details? Considering quicksort animation, we want to express details of each operation (how 2 pointers move, how to compare, how to assign...), must we use a huge object array to keep these processes?
Pure waste, because sorting only wants sorting result, not process details, the process details we worked hard to record are actually used only once then discarded
Change perspective, executing animation during sorting process is most reasonable, during sorting process can get all details, execute animation, then next step sorting
But problem is animation is doing, sorting is also doing, after Step1 animation completes, sorting has already proceeded to Step5. Yes, we need to let JS stop
1. Let JS Stop
Using yield is simplest, F12 input function* + yield syntax, pause however you want, for more information about ES6 yield, please check [generator (Generator)_ES6 Notes 2](/articles/generator(生成器)-es6 笔记 2/)
Note, now time is 6 years ago (2010-6), JS just entered ES5 era, no yield available, at that time most people and I felt JS couldn't stop (such as how to pause loops)
Indeed, loops can't stop, but recursion can completely pause
Simulate an asynchronous animation (CSS animation etc.), as follows:
var asyncAnim = function(str, callback) {
console.log(str);
setTimeout(function() {
// Time-consuming animation
//...
callback();
}, 100);
};
Then change loop to recursion:
var uniqueWithAnim = function(arr, callback) {
var dir = {};
var _arr = arr.slice();
var res = [];
var animLock = false;
// Use recursion instead of loop
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);
// Animation
animLock = true;
asyncAnim('push ' + item, function() {
// Animation complete callback
animLock = false;
find();
});
}
i++;
if (!animLock) {
find();
}
};
// Start recursion
find();
};
Test it:
// 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
Effect is JS is stopped, we connected asynchronous logic and synchronous logic through callbacks, but need to rewrite code, not beautiful at all
2. Simulate yield
What we want is a general tool pattern that can let JS stop, so need to simulate implementation of 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 is a wrapper, inject parameters into fn, $yield is used to build step chain
Try it:
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 }
This is a simple yield implementation method, doesn't look beautiful either
3. WindJS Principles
-
Recursion instead of loops. Split logic blocks into step chains, delay calling next function, this is so-called pause (
Sleep) -
"Compile" hides code rewriting details. Wind rewrites source code for us, so effect is we can write asynchronous code in synchronous form
Wind doesn't change user's thinking mode and coding habits, also doesn't impose a bunch of APIs, but implements through "implicit" source code rewriting method, so looks like modified JS language itself, respond to all changes with immutability, no need to provide large and comprehensive solutions like async module
P.S. Now ES7 has already absorbed this solution (provided async&await), looking at it this way, Wind indeed "modified" JS language itself
P.S. If frameworks are all designed according to this train of thought, FEers wouldn't have so many things to learn... If language itself absorbs various features and tends towards perfection, wouldn't need frameworks, at most still need engineering tools (such as build tools etc.)
II. "Compiling" Source Code
"Compiling" source code is JS ultimate black magic, like a door shrouded in black qi, representing infinite possibilities
1. Wind Compilation Example
Reading file triggers exception, example as follows:
var fs = require('fs');
var Wind = require('Wind');
// Task model captures asynchronous exceptions
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);
}
}));
// Get task object
var task = readFile();
// Start task
task.start();
Compilation result is as follows:
(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 completed rewriting for us, changed synchronous form code to asynchronous callback form
2. Simulate Capturing Exceptions in Asynchronous Callbacks
Wind's try...catch can capture exceptions in asynchronous callbacks, looks very mysterious, actually principle is very simple
// Try to capture exceptions in asynchronous callbacks
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 internally is similar to above example, it's just we were blinded by compile, that's why we can't see through this layer of mystery
3. "Compilation"
Our simulated ex.Try doesn't look as elegant as Wind's native try...catch, no problem, let's also try "compiling" beauty method:
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);
}
};
};
Finally, test effect:
// 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:...
Form and effect are completely same, this is the secret of Wind's try...catch being able to capture asynchronous callback exceptions
"Compilation" completed work is as follows:
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);
}
Simply put it's string concatenation, that's all
Similarly, Wind being able to handle infinite sequences is nothing strange, example as follows:
// 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) infinite loop is replaced with dead recursion containing breakpoints, just like ES6's 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
III. Summary
Wind's features are as follows:
-
Compile source code
-
Task model
-
Write asynchronous code in synchronous form
Applicable scenarios: Suitable for migrating from existing synchronously written code to Node, can save cost of rewriting code
So, can ES7's async&await replace Wind?
Yes, because implementation principles and goals are consistent
-
Implementation principle:
yieldpause -
Goal: Write asynchronous code in synchronous form
ES7's async&await came through promise, generator all the way, while Wind saw this day 5 years ago, and implemented the vision in advance
Learn the technical ideas themselves, not just code application, this is programming
Reference Materials
- "Deep Dive NodeJS"
No comments yet. Be the first to share your thoughts.