JavaScript的sleep實現--Javascript異步編程學習


一、原始需求

最近在做百度前端技術學院的練習題,有一個練習是要求遍歷一個二叉樹,並且做遍歷可視化即正在遍歷的節點最好顏色不同

二叉樹大概長這個樣子:

以前序遍歷為例啊,

每次訪問二叉樹的節點加個sleep就好了?

筆者寫出來是這樣的:

 1 let root = document.getElementById('root-box');
 2 
 3   function preOrder (node) {
 4         if (node === undefined) {
 5             return;
 6         }
 7         node.style.backgroundColor = 'blue';//開始訪問
 8         sleep(500);
 9         node.style.backgroundColor = '#ffffff';//訪問完畢
10         preOrder(node.children[0]);
11         preOrder(node.children[1]);
12     }
13 
14     document.getElementById('pre-order').addEventListener('click', function () {
15         preOrder(root);
16     });

問題來了,JavaScript里沒有sleep函數!

二、setTimeout實現

了解JavaScript的並發模型 EventLoop 的都知道JavaScript是單線程的,所有的耗時操作都是異步的

可以用setTimeout來模擬一個異步的操作,用法如下:

 setTimeout(function(){ console.log('異步操作執行了'); },milliSecond); 

意思是在milliSecond毫秒后console.log會執行,setTimeout的第一個參數為回調函數,即在過了第二個參數指定的時間后會執行一次。

如上圖所示,Stack(棧)上是當前執行的函數調用棧,而Queue(消息隊列)里存的是下一個EventLoop循環要依次執行的函數。

實際上,setTimeout的作用是在指定時間后把回調函數加到消息隊列的尾部,如果隊列里沒有其他消息,那么回調會直接執行。即setTimeout的時間參數僅表示最少多長時間后會執行。

更詳細的關於EventLoop的知識就不再贅述,有興趣的可以去了解關於setImmediate和Process.nextTick以及setTimeout(f,0)的區別

據此寫出了實際可運行的可視化遍歷如下:

  let root = document.getElementById('root-box');
    let count = 1;
    //前序
    function preOrder (node) {
        if (node === undefined) {
            return;
        }

        (function (node) {
            setTimeout(function () {
                node.style.backgroundColor = 'blue';
            }, count * 1000);
        })(node);

        (function (node) {
            setTimeout(function () {
                node.style.backgroundColor = '#ffffff';
            }, count * 1000 + 500);
        })(node);

        count++;
        preOrder(node.children[0]);
        preOrder(node.children[1]);
    }

 document.getElementById('pre-order').addEventListener('click', function () {
        count = 1;
        preOrder(root);
    });

可以看出我的思路是把遍歷時的顏色改變全部變成回調,為了形成時間的差距,有一個count變量在隨遍歷次數遞增。

這樣看起來是比較清晰了,但和我最開始想像的sleep還是差別太大。

sleep的作用是阻塞當前進程一段時間,那么好像在JavaScript里是很不恰當的,不過還是可以模擬的

三、Generator實現

 在學習《ES6標准入門》這本書時,依稀記得generator函數有一個特性,每次執行到下一個yield語句處,yield的作用正是把cpu控制權交出外部,感覺可以用來做sleep。

寫出來是這樣:

let root = document.getElementById('root-box');

  function* preOrder (node) {
        if (node === undefined) {
            return;
        }
        node.style.backgroundColor = 'blue';//訪問
        yield 'sleep';
        node.style.backgroundColor = '#ffffff';//延時
        yield* preOrder(node.children[0]);
        yield* preOrder(node.children[1]);
    }

    function sleeper (millisecond, Executor) {
        for (let count = 1; count < 33; count++) {
            (function (Executor) {
                setTimeout(function () {
                    Executor.next();
                }, count * millisecond);
            })(Executor);
        }
    }

    document.getElementById('pre-order').addEventListener('click', function () {
        let preOrderExecutor = preOrder(root);
        sleeper(500, preOrderExecutor);
    });

這種代碼感覺很奇怪,像是為了用generator而用的(實際上也正是這樣。。。),相比於之前的setTimeout好像沒什么改進之處,還是有一個count在遞增,而且必須事先知道遍歷次數,才能引導generator函數執行。問題的關鍵在於讓500毫秒的遍歷依次按順序執行才是正確的選擇。

四、Generator+Promise實現

為了改進,讓generator能夠自動的按照500毫秒執行一次,借助了Promise的resolve功能。使用thunk函數的回調來實現應該也是可以的,不過看起來Promise更容易理解一點

思路就是,每一次延時是一個Promise,指定時間后resolve,而resolve的回調就將Generator的指針移到下一個yield語句處。

  let root = document.getElementById('root-box');

    function sleep (millisecond) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                resolve('wake');
            }, millisecond);
        });
    }

    function* preOrder (node) {
        if (node === undefined) {
            return;
        }
        node.style.backgroundColor = 'blue';//訪問
        yield sleep(500);//返回了一個promise對象
        node.style.backgroundColor = '#ffffff';//延時
        yield* preOrder(node.children[0]);
        yield* preOrder(node.children[1]);
    }


    function executor (it) {

        function runner (result) {
            if (result.done) {
                return result.value;
            }
            return result.value.then(function (resolve) {
                runner(it.next());//resolve之后調用
            }, function (reject) {
                throw new Error('useless error');
            });
        }

        runner(it.next());
    }

    document.getElementById('pre-order').addEventListener('click', function () {
        let preOrderExecutor = preOrder(root);
        executor(preOrderExecutor);
    });

看起來很像原始需求提出的sleep的感覺了,不過還是需要自己寫一個Generator的執行器

五、Async實現

ES更新的標准即ES7有一個async函數,async函數內置了Generator的執行器,只需要自己寫generator函數即可

let root = document.getElementById('root-box');

  function sleep (millisecond) {
        return new Promise(function (resovle, reject) {
            setTimeout(function () {
                resovle('wake');
            }, millisecond);
        });
    }

    async function preOrder (node) {
        if (node === undefined) {
            return;
        }
        let res = null;
        node.style.backgroundColor = 'blue';
        await sleep(500);
        node.style.backgroundColor = '#ffffff';
        await preOrder(node.children[0]);
        await preOrder(node.children[1]);
    }


    document.getElementById('pre-order').addEventListener('click', function () {
        preOrder(root);
    });

大概只能做到這一步了,sleep(500)前面的await指明了這是一個異步的操作。

不過我更喜歡下面這種寫法:

 let root = document.getElementById('root-box');

    function visit (node) {
        node.style.backgroundColor = 'blue';
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                node.style.backgroundColor = '#ffffff';
                resolve('visited');
            }, 500);
        });
    }

    async function preOrder (node) {
        if (node === undefined) {
            return;
        }

await visit(node); await preOrder(node.children[0]); await preOrder(node.children[1]); } document.getElementById('pre-order').addEventListener('click', function () { preOrder(root); });

不再糾結於sleep函數的實現了,visit更符合現實中的情景,訪問節點是一個耗時的操作。整個代碼看起來清晰易懂。

經過這次學習,體會到了JavaScript異步的思想,所以,直接硬套C語言的sleep的概念是不合適的,JavaScript的世界是異步的世界,而async出現是為了更好的組織異步代碼的書寫,思想仍是異步的

 

在下初出茅廬,文章中有什么不對的地方還請不吝賜教

參考文獻:

1、《ES6標准入門》

2、JavaScript並發模型與Event Loop:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop


免責聲明!

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



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