標籤:c函數 ++ cond ret pre alt site ntb stack
一、原始需求
最近在做百度前端技術學院的練習題,有一個練習是要求遍曆一個二叉樹,並且做遍曆可視化即正在遍曆的節點最好顏色不同
二叉樹大概長這個樣子:
以前序走訪為例啊,
每次訪問二叉樹的節點加個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
JavaScript的sleep實現--Javascript非同步編程學習