一種模仿線程的Javascript異步模型設計&實現


jQuery中所支持的異步模型為:

  • Callbacks,回調函數列隊。
  • Deferred,延遲執行對象。
  • Promise,是Deferred只暴露非狀態改變方法的對象。

這些模型都很漂亮,但我想要一種更帥氣的異步模型。

 

Thread?

我們知道鏈式操作是可以很好的表征運行順序的(可以參考我的文章《jQuery鏈式操作》),然而通常基於回調函數或者基於事件監聽的異步模型中,代碼的執行順序不清晰。

Callbacks模型實際上類似一個自定義事件的回調函數隊列,當觸發該事件(調用Callbacks.fire())時,則回調隊列中的所有回調函數。

Deferred是個延遲執行對象,可以注冊Deferred成功、失敗或進行中狀態的回調函數,然后通過觸發相應的事件來回調函數。

這兩種異步模型都類似於事件監聽異步模型,實質上順序依然是分離的。

當然Promise看似能提供我需要的東西,比如Promise.then().then().then()。但是,Promise雖然成功用鏈式操作明確了異步編程的順序執行,但是沒有循環,成功和失敗分支是通過內部代碼確定的。

個人認為,Promise是為了規范化后端nodejs中I/O操作異步模型的,因為I/O狀態只有成功和失敗兩種狀態,所以他是非常成功的。

但在前端,要么只有成功根本沒有失敗,要么不止只有兩種狀態,不應當固定只提供三種狀態的方案,我覺得應該提供可表征多狀態的異步方案。

這個大家可以在something more看到。

我想要一種類似於線程的模型,我們在這里稱為Thread,也就是他能順序執行、也能循環執行、當然還有分支執行。

 

順序執行

線程的順序執行流程,也就是類似於:

do1();
do2();
do3();

這樣就是依次執行do1,do2,do3。因為這是異步模型,所以我們希望能添加wait方法,即類似於:

do1();
wait(1000);    //等待1000ms
do2();
wait(1000);    //等待1000ms
do3();
wait(1000);    //等待1000ms

不使用編譯方法的話,使用鏈式操作來表征順序,則實現后的樣子應當是這樣的:

Thread().    //獲取線程
then(do1).    //然后執行do1
wait(1000).    //等待1000ms
then(do2).    //然后執行do2
wait(1000).    //等待1000ms
then(do3).    //然后執行do3
wait(1000);    //等待1000ms

 

循環執行

循環這很好理解,比如for循環:

for(; true;){
    dosomething();
    wait(1000);
}

進行無限次循環執行do,並且每次都延遲1000ms。則其鏈式表達應當是這樣的:

Thread().    //獲取線程
loop(-1).    //循環開始,正數則表示循環正數次,負數則表示循環無限次
    then(dosomething).    //然后執行do
    wait(1000).    //等待1000ms
loopEnd();    //循環結束

這個可以參考后面的例子。 

 

分支執行

分支也就是if...else,比如:

if(true){
    doSccess();
}else{
    doFail();
}

那么其鏈式實現應當是:

Thread().    //獲得線程
right(true).    //如果表達式正確
    then(doSccess).    //執行doSccess
left().    //否則
    then(doFail).    //執行doFail
leftEnd().    //left分支結束
rightEnd();    //right分支結束

 

聲明變量

聲明變量也就是:

var a = "hello world!";

可被其它函數使用。那么我們的實現是:

Thread().    //得到線程
define("hello world!").    //將回調函數第一個參數設為hello world!
then(function(a){alert(a);});    //獲取變量a,alert出來

 

順序執行實現方案

Thread實際上是一個打包函數Fn隊列。

所謂打包函數就是將回調函數打包后產生的新的函數,舉個例子:

function package(callback){
    return function(){
        callback();
        // 干其他事情
    }
}

這樣我們就將callback函數打包起來了。

Thread提供一個fire方法來觸發線程取出一個打包函數然后執行,打包函數執行以后回調Thread的fire方法。

那么我們就可以順序執行函數了。

現在只要打包的時候設置setTimeout執行,則這個線程就能實現wait方法了。

 

循環執行實現方案

循環Loop是一個Thread的變形,只不過在執行里面的打包函數的時候使用另外一種方案,通過添加一個指針取出,執行完后觸發Loop繼續,移動指針取出下一個打包函數。

 

分支執行實現方案

分支Right和Left也是Thread的一種變形,開啟分支的時候,主Thread會創建兩個分支Right線程和Left線程,打包一個觸發分支Thread的函數推入隊列,然后當執行到該函數的時候判斷觸發哪個分支執行。

其中一個隊列執行結束后回調主Thread,通知進行下一步。 

 

例子

由於該方案和wind-asycn非常相似,所以我們拿wind.js中的clock例子進行改造看看其中的差別吧。

wind.js中的例子:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>Clock - Wind.js Sample</title>
    <meta http-equiv="X-UA-Compatible" content="IE=9" />    
    
    <script src="http://www.cnblogs.com/../src/wind-core.js"></script>
    <script src="http://www.cnblogs.com/../src/wind-compiler.js"></script>
    <script src="http://www.cnblogs.com/../src/wind-builderbase.js"></script>
    <script src="http://www.cnblogs.com/../src/wind-async.js"></script>

    <script>
        var drawHand = function(value, length) {
            ctx.beginPath();
            
            var angle = Math.PI * 2 * value / 60;
            var x = Math.sin(angle) * length;
            var y = Math.cos(angle) * length;
            
            ctx.moveTo(100, 100);
            ctx.lineTo(100 + x, 100 - y);
            ctx.stroke();
        }
    
        var drawClock = function(time) {
            if (!ctx) {
                var h = time.getHours();
                var m = time.getMinutes();
                var s = time.getSeconds();
                
                var text = 
                    ((h >= 10) ? h : "0" + h) + ":" +
                    ((h >= 10) ? m : "0" + m) + ":" +
                    ((h >= 10) ? s : "0" + s);
                
                document.getElementById("clockText").innerHTML = text;
                return;
            }
        
            ctx.clearRect(0, 0, 200, 200);
            
            ctx.beginPath();
            ctx.arc(100, 100, 90, 0, Math.PI * 2, false);
            for (var i = 0; i < 60; i += 5) {
                var angle = Math.PI * 2 * i / 60;
                var x = Math.sin(angle);
                var y = Math.cos(angle);
                ctx.moveTo(100 + x * 85, 100 - y * 85);
                ctx.lineTo(100 + x * 90, 100 - y * 90);
            }
            ctx.stroke();
            
            drawHand(time.getSeconds(), 80);
            drawHand(time.getMinutes() + time.getSeconds() * 1.0 / 60, 60);
            drawHand(time.getHours() % 12 * 5 + time.getMinutes() * 1.0 / 12, 40);
        }
    
        var drawClockAsync = eval(Wind.compile("async", function(interval) {
            while (true) {
                drawClock(new Date());
                $await(Wind.Async.sleep(interval));
            }
        }));
    </script>
</head>
<body>
    <canvas id="clockCanvas" height="200" width="200">
        <div id="clockText" style="font-size:20pt;"></div>
    </canvas>
    <script>
        var canvas = document.getElementById("clockCanvas");
        var ctx = canvas.getContext ? canvas.getContext("2d") : null;
        drawClockAsync(1000).start();
    </script>
</body>
</html>

  

我的例子:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>Clock - asThread.js Sample</title>
    <meta http-equiv="X-UA-Compatible" content="IE=9" />    
    <!-- 例子修改自wind.js -->
    <script src="asThread.js"></script>

    <script>
        var drawHand = function(value, length) {
            ctx.beginPath();
            
            var angle = Math.PI * 2 * value / 60;
            var x = Math.sin(angle) * length;
            var y = Math.cos(angle) * length;
            
            ctx.moveTo(100, 100);
            ctx.lineTo(100 + x, 100 - y);
            ctx.stroke();
        }
    
        var drawClock = function() {
            var time = new Date()
            if (!ctx) {
                var h = time.getHours();
                var m = time.getMinutes();
                var s = time.getSeconds();
                
                var text = 
                    ((h >= 10) ? h : "0" + h) + ":" +
                    ((h >= 10) ? m : "0" + m) + ":" +
                    ((h >= 10) ? s : "0" + s);
                
                document.getElementById("clockText").innerHTML = text;
                return;
            }
        
            ctx.clearRect(0, 0, 200, 200);
            
            ctx.beginPath();
            ctx.arc(100, 100, 90, 0, Math.PI * 2, false);
            for (var i = 0; i < 60; i += 5) {
                var angle = Math.PI * 2 * i / 60;
                var x = Math.sin(angle);
                var y = Math.cos(angle);
                ctx.moveTo(100 + x * 85, 100 - y * 85);
                ctx.lineTo(100 + x * 90, 100 - y * 90);
            }
            ctx.stroke();
            
            drawHand(time.getSeconds(), 80);
            drawHand(time.getMinutes() + time.getSeconds() * 1.0 / 60, 60);
            drawHand(time.getHours() % 12 * 5 + time.getMinutes() * 1.0 / 12, 40);
            
        }
        
        Thread().    // 使用主線程線程
        loop(-1).    // 負數表示循環無限多次,如果是正數n,則表示n次循環
            then(drawClock). // 循環中運行drawClock
            wait(1000).    // 然后等待1000ms
        loopEnd();    // 循環結束
        // 線程定義結束
    </script>
</head>
<body>
    <canvas id="clockCanvas" height="200" width="200">
        <div id="clockText" style="font-size:20pt;"></div>
    </canvas>
    <script>
        var canvas = document.getElementById("clockCanvas");
        var ctx = canvas.getContext ? canvas.getContext("2d") : null;
        Thread().run();    // 運行線程
    </script>
</body>
</html>

  

Something more?

  • 將事件當成分支處理

我們提供了on方法將事件轉成分支來執行。

舉個例子頁面有個按鈕“點我”,但是我們希望打開頁面5秒內單擊沒有效,5秒后顯示“請點擊按鈕”后,單擊才會出現“你成功點擊了”。

使用on分支是這樣的:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>on - asThread.js Sample</title>
    <meta http-equiv="X-UA-Compatible" content="IE=9" />    
    <script src="asThread.js"></script>
</head>
<body>
    <button id = "b">點我</button>
    <script>
        var ele = document.getElementById("b");
    
        Thread().    // 獲得線程
        then(function(){alert("請點擊按鈕")}, 5000).    //然后等5秒顯示"請點擊按鈕"
        on(ele, "click").    // 事件分支On開始,如果ele觸發了click事件
            then(function(){alert("你成功點擊了")}).    //那么執行你成功點擊了
        onEnd().    // 事件分支On結束
        then(function(){alert("都說可以的了")}).    // 然后彈出"都說可以的了"
        run();    //啟動線程
    </script>
</body>
</html>

自定義事件也可以哦,只要在.on時候傳進去注冊監聽函數,和刪除監聽函數就行了。比如:

function addEvent(__elem, __type, __handler){
    //添加監聽
}

function removeEvent(__elem, __type, __handler){
    //刪除監聽
}

Thread().
on(ele, "success", addEvent, removeEvent).
    then(function(){alert("成功!")}).
onEnd().
run();

當然實際上我們還可以注冊多個事件分支。事件分支是並列的,也就是平級的事件分支沒有現有順序,所以我們能這樣:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>on - asThread.js Sample</title>
    <meta http-equiv="X-UA-Compatible" content="IE=9" />    
    <script src="asThread.js"></script>
</head>
<body>
    <button id = "b">點我</button>
    <button id = "c">點我</button>
    <script>
        var ele0 = document.getElementById("b"),
              ele1 = document.getElementById("c");
    
        Thread().    // 獲得線程
        then(function(){alert("請點擊按鈕")}, 5000).    //然后等5秒顯示"請點擊按鈕"
        on(ele0, "click").    // 事件分支On開始,如果ele0觸發了click事件
            then(function(){alert("你成功點擊了")}).    //那么執行你成功點擊了
        on(ele1, "click").    // 事件分支On開始,如果ele1觸發了click事件
            then(function(){alert("你成功點擊了")}).    //那么執行你成功點擊了
        onEnd().    // 事件分支On結束
        then(function(){alert("都說可以的了")}).    // 然后彈出"都說可以的了"
        run();    //啟動線程
    </script>
</body>
</html>    
  • 開辟多個線程

一個線程不夠用?只要輸入名字就能開辟或者得到線程了。

系統會自動初始化一個主線程,當不傳參數時就直接返回主線程:

Thread() //得到主線程

但如果主線程正在用想開辟一個線程時,只要給個名字就行,比如:

Thread("hello")    //得到名字是hello的線程

那么下次再想用該線程時只要輸入相同的名字就行了:

Thread("hello")    //得到hello線程

默認只最多只提供10個線程,所以用完記得刪掉:

Thread("hello").del();
  • setImmediate

IE10已經提供了setImmediate方法,而其他現代瀏覽器也可以模擬該方法,其原理是推倒線程末端,使得瀏覽器畫面能渲染,得到比setTimeout(0)更快的響應。

我們通過接口.imm來提供這一功能。比如:

Thread().
imm(function(){alert("hello world")}).
run();

這方法和.then(fn)不太一樣,.then(fn)是可能阻塞當前瀏覽器線程的,但.imm(fn)是將處理推到瀏覽器引擎列隊末端,排到隊了在運行。

所以如果你使用多個Thread(偽多線程),而又希望確保線程是並行運行的,那么請使用.imm來替代.then。

當然對於老版IE,只能用setTimeout(0)替代了。

  • 分支參數可以是函數

分支Right傳的參數如果只是布爾值肯定很不爽,因為這意味着分支是靜態的,在初始化時候就決定了,但我們希望分支能在執行到的時候再判斷是走Right還是Left,所以我們提供了傳參可以是函數(但是函數返回值需要是布爾值,否則……╮(╯▽╰)╭也會轉成布爾值的……哈哈)。比如:

fucntion foo(boolean){
    return !boolean;
}

Thread().
define(true).
right(foo).
    then(function(){/*這里不會運行到*/}).
rightEnd().
run();

Enjoy yourself!!

 

項目地址

https://github.com/miniflycn/asThread

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM