一、原始需求
最近在做百度前端技术学院的练习题,有一个练习是要求遍历一个二叉树,并且做遍历可视化即正在遍历的节点最好颜色不同
二叉树大概长这个样子:
以前序遍历为例啊,
每次访问二叉树的节点加个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