서두
老趙(Jeffrey Zhao) 는笔者가 존경하는, 철봉 20 회를 할 수 있는 선배입니다
당시博客園에있을때老趙의.Net 기사를 몇 편 읽고, 후에 프런트엔드를 선택하여 JS 를 자세히 배우기 위해 yield 시리즈 기사를 자세히 읽고, 순식간에 고산앙지 (그분이야말로 프로그래밍이라고 부를 만하고, 笔者의 것은코드 응용에 불과합니다)
그 후 github 에서 老趙를 팔로우했습니다 (현재 笔者가 팔로우하는 것은 2 명뿐), 대부분은 기술에 대한 경의에서, 또一小半은 老趙의 学生资助计划 때문입니다 (현재는 WindJS 처럼 역사에 진봉되어 있지만, 책임감이 있고 실사를 하는 사람에게 경의를 표해야 합니다)
일.WindJS 의 기본 원리
Wind.js 的确是个"轮子",但绝对不是"重新发明"的轮子。我不会掩饰对 Wind.js 的"自夸":Wind.js 在 JavaScript 异步编程领域绝对是一个创新,可谓前无来者。有朋友就评价说"在看到 Wind.js 之前,真以为这是不可能实现的",因为 Wind.js 事实上是用类库的形式"修补"了 JavaScript 语言,也正是这个原因,才能让 JavaScript 异步编程体验获得质的飞跃。
我们现在想要解决的是"流程控制"问题,那么说起在 JavaScript 中进行流程控制,有什么能比 JavaScript 语言本身更为现成的解决方案呢?在我看来,流程控制并非是一个理应使用类库来解决的问题,它应该是语言的职责。只不过在某些时候,我们无法使用语言来表达逻辑或是控制流程,于是退而求其次地使用"类库"来解决这方面的问题。
이상은 Wind.js 作者老趙에게의 인터뷰 (상):유래,思路및 발전 에서 인용
기본 원리는 1 개의 예에서 시작합니다.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 개의 포인터가 어떻게 이동하고, 어떻게 비교하고, 어떻게 대입하는지...) 를 표현하고 싶다면, 거대한 오브젝트 배열로 이러한 프로세스를 보유해야 할까요?
순수한 낭비입니다. 소트는 결과를 원하는 것이지, 프로세스 상세가 아니기 때문입니다. 고생해서 기록한 프로세스 상세는 실제로는 1 회 사용했을 뿐이고 버려집니다
각도를 바꿔, 소트 프로세스 중에 애니메이션을 실행하는 것이 가장 합리적입니다. 소트 프로세스 중에 모든 상세를 취득할 수 있고, 애니메이션을 실행하고, 다음에 소트를 진행합니다
그러나 문제는 애니메이션이 실행 중이고, 소트도 실행 중입니다.Step1 의 애니메이션이 완료된 후, 소트는 이미 Step5 까지 진행되었습니다.네, JS 를 정지시켜야 합니다
###1.JS 를 정지시키다
yield 를 사용하는 것은 매우 간단합니다.F12 에서 function* + yield 구문을 입력하고, 어떻게 일시 정지하고 싶은지 자유롭게 할 수 있습니다.ES6 yield 에 대한 더 많은 정보는, [generator(생성기)_ES6 노트 2](/articles/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》
아직 댓글이 없습니다