본문으로 건너뛰기

WindJS 에 경의를 표하다_Node 비동기 플로우 제어 4

무료2016-07-07#Node#WindJS#Jscex#Wind#WindJS原理

기술 사상 자체를 배우고, 단순한 코드 응용이 아닌, 이것이야말로 프로그래밍

서두

老趙(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》

댓글

아직 댓글이 없습니다

댓글 작성