一、原始需求

最近在做百度前端技术学院的练习题,有一个练习是要求遍历一个二叉树,并且做遍历可视化即正在遍历的节点最好颜色不同

二叉树大概长这个样子:

以前序遍历为例啊,

每次访问二叉树的节点加个sleep就好了?

笔者写出来是这样的:

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

   function preOrder (node) {
if (node === undefined) {
return;
}
node.style.backgroundColor = 'blue';//开始访问
sleep(500);
node.style.backgroundColor = '#ffffff';//访问完毕
preOrder(node.children[0]);
preOrder(node.children[1]);
} document.getElementById('pre-order').addEventListener('click', function () {
preOrder(root);
});

问题来了,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

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

  1. javascript异步编程学习及实例

    所谓异步就是指给定了一串函数调用a,b,c,d,各个函数的执行完结返回过程并不顺序为a->b->c->d(这属于传统的同步编程模式),a,b,c,d调用返回的时机并不确定. 对于js ...

  2. C# 异步编程学习(一)

    异步 编程 可在 等待 某个 任务 完成时, 避免 线程 的 占用, 但要 想 正确地 实现 编程, 仍然 十分 伤脑筋. . NET Framework 中, 有三种 不同 的 模型 来 简化 异步 ...

  3. ES6/7 异步编程学习笔记

    前言 在ES6的异步函数出现之前,Js实现异步编程只有settimeout.事件监听.回调函数等几种方法 settTmeout 这种方法常用于定时器与动画的功能,因为其本质上其实是浏览器的WebAPI ...

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

    DOM编程不只能够查找三种节点,也能够操作节点.那就是创建,插入,删除.替换和复制节点.先来看节点 操作方法: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQ ...

  5. C#异步编程学习笔记之-async和await(续)

    书接上文,本篇主要记录的内容要点:1.针对async和await在实际应用中的使用方式:2.异步方法返回值(有返回值和无返回值)的两种情况: 示例一(无返回值): using System; usin ...

  6. C#异步编程学习笔记之-async和await

    一.异步方法介绍(async和await):如果使用async修饰符将某种方法指定为异步方法,即启用以下两种功能.1.标记的异步方法可以使用await来指定暂停点.await运算符通知编译器异步方法: ...

  7. .NET 4.5 Task异步编程学习资料

    参考资料: 1. http://www.cnblogs.com/heyuquan/archive/2013/04/18/3028044.html

  8. 我了解到的JavaScript异步编程

    一. 一道面试题 前段时间面试,考察比较多的是js异步编程方面的相关知识点,如今,正好轮到自己分享技术,所以想把js异步编程学习下,做个总结. 下面这个demo 概括了大多数面试过程中遇到的问题: f ...

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

    JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...

随机推荐

  1. python 装饰器 一篇就能讲清楚

    装饰器一直是我们学习python难以理解并且纠结的问题,想要弄明白装饰器,必须理解一下函数式编程概念,并且对python中函数调用语法中的特性有所了解,使用装饰器非常简单,但是写装饰器却很复杂.为了讲 ...

  2. 详解小白利用eclipse+CDT+MinGW搭建C++开发环境

    安装JDK.安装Eclipse     2.打开Eclipse 找到“help”下的“Eclipse marketplace” 如图:    3.选择相应的C插件: 因为安装过所以显示installe ...

  3. 笔记:Hibernate 查询缓存

    Hibernate 的一级缓存和二级缓存都是对实体进行缓存,他不会缓存普通属性,如果想对普通熟悉进行缓存,可以考虑使用查询缓存. 对于查询缓存来说,他缓存的Key就是查询所用的 HQL 或者 SQL ...

  4. dataTable插件的使用

    var table; table = $("#example").dataTable({ "sDom": "<'top'f<'clear' ...

  5. 通过漫画轻松掌握HDFS工作原理

  6. Webpack的加载器

    一.什么是加载器(loaders)loaders 用于转换应用程序的资源文件,他们是运行在nodejs下的函数 使用参数来获取一个资源的来源并且返回一个新的来源(资源的位置),例如:你可以使用load ...

  7. VUE请求本地数据的配置json-server

    VUE开发请求本地数据的配置,早期的vue-lic下面有dev-server.js和dev-client.js两文件,请求本地数据在dev-server.js里配置,最新的vue-webpack-te ...

  8. mysql的存储过程,函数,事件,权限,触发器,事务,锁,视图,导入导出

    1.创建过程 1.1 简单创建 -- 创建员工表 DROP TABLE IF EXISTS employee; CREATE TABLE employee( id int auto_increment ...

  9. qt中控件的使用函数

    1.Text Edit编辑框 //将编辑框中的内容转化成Utf8编码 ui->textEdit->toPlainText().toUtf8(); 2.Combo Box下拉框的应用 (1) ...

  10. 【Alpha】阶段总结报告

    团队成员 陈家权 031502107 赖晓连 031502118 雷晶 031502119 林巧娜 031502125 庄加鑫 031502147 一.项目预期计划及现实进展 项目预期计划 现实进展 ...