原創(chuàng)文章,未經(jīng)許可,請勿轉載
?Soyaine
這兩天找了個沒網(wǎng)的地方,總算把之前遺留下的排序可視化實現(xiàn)了,現(xiàn)在初步完成了冒泡排序和選擇排序,寫這篇是為了整理一下從零到實現(xiàn)的過程,也分享自己的解決思路,歡迎多多交流。
(很牛的視頻鏈接列表。)
下面主要記錄下選擇排序的實現(xiàn)。
失敗的嘗試
還沒開始做時,覺得只要在排序時每一步高亮一個數(shù)據(jù)塊,就能實現(xiàn)了。于是能想到的最簡單的是,寫一個實現(xiàn)暫停的 sleep
函數(shù),在每次循環(huán)后暫停,操作 DOM。我經(jīng)歷過的失敗的嘗試有:
- 在循環(huán)中操作節(jié)點樣式,無法控制延遲時間
- 在循環(huán)中混入
setTimeOut
- 在循環(huán)中混入元素交換的操作
瀏覽器的渲染機制及定時器機制
瀏覽器渲染
瀏覽器是單線程的,UI 引擎和 JS 引擎互斥,也就是說,在循環(huán)運行的過程中, JS 引擎在執(zhí)行循環(huán)操作時,無法調(diào)用 UI 引擎。所以涉及到重新渲染頁面的操作不會立即執(zhí)行,而是會排到任務隊列中,等循環(huán)結束(JS 引擎空閑)時再執(zhí)行渲染。所以能看到的效果是,頁面假死,到最后突然變成排序結束的樣子。
這時候就需要用到定時器,具體用法參見文檔。
在將 setTimeOut
和 for/while
循環(huán)一起使用的過程中,我遇到了兩個問題:
- 時序混亂。它也會在循環(huán)結束后才統(tǒng)一渲染,有可能會出現(xiàn)交叉的情況。
- 參數(shù)無法正確的傳遞。具體的問題是
setTimeOut
內(nèi)函數(shù)真正執(zhí)行時,所讀取到的是循環(huán)結束后的值,而不是想象的每次循環(huán)傳遞一個參數(shù)(參見 stackOverflow 的問題)。
由此進化得到的解決辦法是,把排序和渲染分離,排序時專注于數(shù)據(jù)的循環(huán)比較,得出每一幀所涉及到的節(jié)點索引以及對應狀態(tài),存入隊列備后續(xù)渲染使用,渲染時根據(jù)上一步提供的單幀數(shù)據(jù),用定時器進行 DOM 操作實現(xiàn)一步一步的動畫呈現(xiàn)。
setTimeOut 的循環(huán)實現(xiàn)
之所以會考慮將每幀的情況存入數(shù)列之后,再統(tǒng)一渲染,這是在用 setTimeOut
實現(xiàn)循環(huán)的過程中發(fā)現(xiàn)的。
Nicholas 在《JavaScript高級程序設計》中優(yōu)化性能的思路是將需要長時間運算的循環(huán)分解為“片”來進行運算:
// http://www.nczonline.net/blog/2009/01/13/speed-up-your-javascript-part-1/
function chunk(array, process, context){
var items = array.concat(); //clone the array
setTimeout(function(){
var item = items.shift();
process.call(context, item);
if (items.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
可以注意到其中 setTimeout(arguments.callee, 100)
一句將會為新的數(shù)據(jù)設定一個新的 timer。借用這種方法,我們可以實現(xiàn)數(shù)組元素的遍歷,即存入需要渲染的數(shù)據(jù)之后,只需使用 setTimeOut
依次讀取每條數(shù)據(jù)進行渲染即可。
選擇排序的分解
拿選擇排序來舉例,為便于描述,下面我用外循環(huán)和內(nèi)循環(huán)來指代選擇排序嵌套的兩層循環(huán)。
var arr; //待排序數(shù)組
var min; //存最小值
for (var outer = 0; outer < arr.length-1; outer++){
min = outer;
for (var inner = outer+1; inner < arr.length; inner++){
if (arr[min] > arr[inner]){
min = inner;
}
}
swap(arr, min, inner);
}
下面這張圖是我的解決思路,主要有兩部分,一是 Array(排序數(shù)據(jù))操作,二是 DOM 操作,包括樣式和高度的改變。
DOM 單幀分解
下面來設想一下排序過程中所涉及到的每一幀是以什么樣的樣式出現(xiàn)。可以回看上面的幾個視頻,思考過程是這樣,把整個排序過程分解,每一步也就對應了我們后來的每一幀。
想象我需要實現(xiàn)的效果:
-
arr[0]
高亮 -
arr[1]
高亮 -
if (arr[min] > arr[1])
-
true
:arr[1]
標記為最小值 -
false
:arr[1]
取消高亮
-
-
arr[2]
高亮 - ……循環(huán)
上面的過程中,為了區(qū)分不同類型的數(shù),思考一下有哪些狀態(tài):
- select: 待交換,每輪外循環(huán)所選定的值
- on: 正在比較中,即每輪內(nèi)循環(huán)依次遍歷的值
- min: 內(nèi)循環(huán)的目的是找到此輪的最小值,所以每次比較都會產(chǎn)生當前的最小值,需要區(qū)分
- sorted: 已完成排序,每輪內(nèi)循環(huán)結束并完成交換后,當前外循環(huán)選定的值已完成排序
將以上幾種狀態(tài)對應到不同的 CSS 樣式,之后渲染的時候只需要通過操作樣式表即可實現(xiàn)不同狀態(tài)的標識:
/**
* 渲染每一步
* @param main 外循環(huán)的主數(shù)
* @param div 被比較的用于操作的 div
* @param state 操作涉及的樣式名稱
* @param on 添加或去除樣式、
*/
function renderSelectionDiv(main, div, state, on){
var onDiv = document.getElementById(div.toString());
if(on == 1){
if (state == ""){
// 交換
var outerDiv = document.getElementById(main.toString());
swap(outerDiv, onDiv);
}else{
onDiv.classList.add(state);
}
}else {
onDiv.classList.remove(state);
}
}
Array 排序分解
之所以先寫上面的部分,是為了便于理解。實際過程中,我是先從排序時的循環(huán)過程開始嘗試,然后思考每幀需要的數(shù)據(jù),之后再回過頭來修正這一部分中的數(shù)據(jù)格式。
考慮排序過程中的兩層循環(huán),重點注意下標的變化過程:
- 外循環(huán)是從
0
→數(shù)組倒數(shù)第二位
- 內(nèi)循環(huán)是從
此輪外循環(huán)的值
→數(shù)組最后一位
。
理解循環(huán)的過程不難,較為繁瑣的是理清每一步所需的操作是什么,將上一部分中所設想的樣式變換插入到排序的循環(huán)中,下面是我的代碼:
/**
* 提供外層循環(huán)的 selected 值,及內(nèi)層循環(huán)數(shù)組
* @param outerId
* @param innerQueue
* @param timerQueue
*/
function getMin(outerId, innerQueue, timerQueue){
var outerDiv = data[outerId];
var minId = outerId;
// 選中外層循環(huán)主值
timerQueue.push([outerId, outerId, "select", 1]);
while(innerQueue.length > 0){
// 將需要比較的數(shù)存入數(shù)列
var innerId = innerQueue.shift();
var minDiv = data[minId];
var innerDiv = data[innerId];
timerQueue.push([outerId, innerId, "on", 1]);
if(minDiv > innerDiv){
// 修改最小值
timerQueue.push([outerId, minId, "min", 0]);
timerQueue.push([outerId, minId, "on", 0]);
minId = innerId;
timerQueue.push([outerId, minId, "min", 1]);
}else {
timerQueue.push([outerId, innerId, "on", 0]);
}
}
// 交換
timerQueue.push([outerId, minId, "", 1]);
swapData(outerId, minId);
// 去除最小值標識
timerQueue.push([outerId, minId, "min", 0]);
timerQueue.push([outerId, minId, "on", 0]);
// 已排序標識
timerQueue.push([outerId, outerId, "sort", 1]);
// 去除選中值標識
timerQueue.push([outerId, outerId, "select", 0]);
timerQueue.push([outerId, outerId, "min", 0]);
return timerQueue;
}
后
這里很簡單,但忍不住想要打一個比喻。
想象我們在為電影寫劇本,需要提前寫好每個鏡頭所涉及到的演員和表演場景,演員安排即是 Array 操作部分進行管理,而具體的場景則是 DOM 操作部分。
寫好劇本之后,交給 setTimeOut
,它將按照劇本的要求,召集人員進行拍攝,然后提供給瀏覽器播放出來。
如果上面我說得不是很清楚,那么帶著這樣的想象,回去思考這個過程,會很容易明白。歡迎批評指正。