メインコンテンツへ移動

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》

コメント

コメントはまだありません

コメントを書く