最近在作百度前端技術學院的練習題,有一個練習是要求遍歷一個二叉樹,而且作遍歷可視化即正在遍歷的節點最好顏色不一樣前端
二叉樹大概長這個樣子: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函數!異步
瞭解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裏是很不恰當的,不過仍是能夠模擬的
在學習《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