一、原始需求
最近在做百度前端技術學院的練習題,有一個練習是要求遍歷一個二叉樹,並且做遍歷可視化即正在遍歷的節點最好顏色不同
二叉樹大概長這個樣子:
以前序遍歷為例啊,
每次訪問二叉樹的節點加個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