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

1、原始需求

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

二叉樹大概長這個樣子:node

之前序遍歷爲例啊,promise

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

筆者寫出來是這樣的:less

 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函數!異步

2、setTimeout實現

瞭解JavaScript的併發模型 EventLoop 的都知道JavaScript是單線程的,全部的耗時操做都是異步的async

能夠用setTimeout來模擬一個異步的操做,用法以下:函數

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

意思是在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裏是很不恰當的,不過仍是能夠模擬的

3、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毫秒的遍歷依次按順序執行纔是正確的選擇。

4、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的執行器

5、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出現是爲了更好的組織異步代碼的書寫,思想還是異步的

 

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

參考文獻:

一、《ES6標準入門》

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

相關文章
相關標籤/搜索