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);
    });

这种代码感觉很奇怪,相比于之前的setTimeout好像没什么改进之处,还是有一个count在递增,而且必须事先指导遍历次数,才能引导generator函数执行。

四、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

时间: 2024-10-16 18:43:24

JavaScript的sleep实现--Javascript异步编程学习的相关文章

javascript异步编程学习及实例

所谓异步就是指给定了一串函数调用a,b,c,d,各个函数的执行完结返回过程并不顺序为a->b->c->d(这属于传统的同步编程模式),a,b,c,d调用返回的时机并不确定. 对于js代码来说,这种异步编程尤其重要并大量存在,很大的原因是js为单线程模式,其中包含了ui刷新和响应.想像一下,如果我们写了一段js代码,他会发起ajax请求,如果为同步模式,就意味着这时js单线程一直等待这个ajax完成,这期间是不能响应任何用户交互的,这种体验是不可接受的.现在随着服务端js-nodejs的兴

ES6/7 异步编程学习笔记

前言 在ES6的异步函数出现之前,Js实现异步编程只有settimeout.事件监听.回调函数等几种方法 settTmeout 这种方法常用于定时器与动画的功能,因为其本质上其实是浏览器的WebAPIs功能,因此与主线程并不同,当延时方法到达触发条件时,方法被添加到用于回调的任务队列,只要执行引擎栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些满足触发条件的回调函数.所以其等待时间在某些情况下往往不是那么准备,这一方面推荐可以看看<你不知道的js>这方面的章节. 事件监听 这一个方法

轻松学习JavaScript二十二:DOM编程学习之节点操作

DOM编程不仅仅可以查找三种节点,也可以操作节点,那就是创建,插入,删除,替换和复制节点.先来看节点 操作方法: 还是借用一贯的HTML代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.

轻松学习JavaScript二十四:DOM编程学习之操作CSS样式(一)

CSS样式作为HTML的辅助,可以增强页面的显示效果.前面学了DOM操作HTML,因此也要学DOM操作CSS 样式的方法.在学习CSS的时候,已经知道了CSS插入到HTML文档有三种情况,分别为:行内式,内嵌式和外联式 (这是我们最常用的).下面就来看怎么操作这三种情况: 一操作行内式 先来写一个HTML文档: <span style="font-size:18px;"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0

异步编程学习

asyn修饰符只能用于返回Task或void的方法.它不能用于程序的入口点,即Main方法不能使用async修饰符.await只能用于返回.Task的方法. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 异步 { class Program { static void Main(string[

JavaScript异步编程(2)- 先驱者:jsDeferred

原文出处: linkFly   欢迎分享原创到伯乐头条 JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascript的异步编程,jsDeferred也是有必要探索的:因为Promise/A+规范的制定基本上是奠定在jsDeferred上,它是javascript异步编程中里程碑式的作品.jsDeferred自身的实现也是非常有意思的. 本文将探讨项目js

.net 异步编程async &amp; await关键字的思考

C# 5.0引入了两个关键字 async和await,这两个关键字在很大程度上帮助我们简化了异步编程的实现代码,而且TPL中的task与async和await有很大的关系 思考了一下异步编程中的async & await关键字,对两个关键字尤其是await关键字一直很迷糊,因此深入思考了一下.首先借助的示例是:[你必须知道的异步编程]C# 5.0 新特性--Async和Await使异步编程更简单这是博客园一个大牛写的,自己也一直关注这个大神,不得不说,博客园大神很多,而且氛围也很好.我引入了其中

JavaScript学习--Item27 异步编程异常解决方案

1.JavaScript异步编程的两个核心难点 异步I/O.事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络.文件访问功能,且使之在后端实现了较高的性能.然而异步风格也引来了一些麻烦,其中比较核心的问题是: 1.函数嵌套过深 JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成 金字塔型结构.这不仅使得代码变难看难懂,更使得调试.重构的过程充满风险. 2.异常处理 回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂.这

前端排序算法总结;前端面试题2.0;JavaScript异步编程

1.前端 排序算法总结 排序算法可能是你学编程第一个学习的算法,还记得冒泡吗? 当然,排序和查找两类算法是面试的热门选项.如果你是一个会写快排的程序猿,面试官在比较你和一个连快排都不会写的人的时候,会优先选择你的.那么,前端需要会排序吗?答案是毋庸置疑的,必须会.现在的前端对计算机基础要求越来越高了,如果连排序这些算法都不会,那么发展前景就有限了.本篇将会总结一下,在前端的一些排序算法. https://segmentfault.com/a/11... 2.前端面试题 V2.0 详见: 这是一份