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

posted @ 2017-03-30 14:46 pompeybrain 阅读(...) 评论(...) 编辑 收藏