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

相关文章
相关标签/搜索