「注釋」作者在本文里沒有說明這么一個事實:
目前的版本Lo-Dash v2.4.1
并沒有引入延遲求值的特性,Lo-Dash 3.0.0-pre
中部分方法進行了引入,比如filter()
,map()
,reverse()
。
原文
我時常覺得像Lo-Dash
這樣優秀的庫已經無法再優化了。它整合了各種奇技淫巧已經將JavaScript的性能開發到了極限。它使用了最快速的語句,優化的算法,甚至還會在發版前做性能測試以保證回歸沒問題。
延遲求值
但似乎我錯了-還可以讓Lo-Dash
有明顯的提升。只需將關注點從細微的優化轉移到算法上來。譬如,在一次循環中我們往往會去優化循環體:
var len = getLength();
for(var i = 0; i < len; i++) {
operation(); // <- 10ms - 如何做到 9ms?!
}
但針對循環體的優化往往很難,很多時候已經到極限了。相反,優化getLength()
函數盡量減少循環次數變得更有意義了。你想啊,這個數值越小,需要循環的10ms
就越少。
這便是Lo-Dash
實現延遲求值的大致思路。重要的是減少循環次數,而不是每次循環的時間。讓我們考察下面的例子:
function priceLt(x) {
return function(item) { return item.price < x; };
}
var gems = [
{ name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 },
{ name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7 },
{ name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 },
{ name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 }
];
var chosen = _(gems).filter(priceLt(10)).take(3).value();
我們只想取出3個價格低于10元的小球。通常情況下我們先過濾整個數據源,最后從所有小于10的元素里返回前面三個即可。
但這種做法并不優雅。它處理了全部8個數據,但其實只需要處理前面5個我們就能拿到結果了。同樣為了得到正確的結果,延遲求值則只處理最少的元素。優化后如下圖所示:
一下子就獲得了37.5%的性能提升。很容易找出提升X1000+的例子。比如:
var phoneNumbers = [5554445555, 1424445656, 5554443333, … ×99,999];
// 取出100個含 `55` 的號碼
function contains55(str) {
return str.contains("55");
};
var r = _(phoneNumbers).map(String).filter(contains55).take(100);
這個例子中map
和filter
將遍歷99999 個元素,但很有可能我們只需處理到1000個元素的時候就已經拿到想要的結果了。這回性能的提升就太明顯了(benchmark):
流水線
延遲求值同時帶來了另一個好處,我稱之為“流水線”。要旨就是避免產生中間數組,而是對一個元素一次性進行完所有操作。下面用代碼說話:
var result = _(source).map(func1).map(func2).map(func3).value();
上面看似優雅的寫法在原始的Lo-Dash
里會轉換成下面的樣子(直接求值):
var result = [], temp1 = [], temp2 = [], temp3 = [];
for(var i = 0; i < source.length; i++) {
temp1[i] = func1(source[i]);
}
for(i = 0; i < source.length; i++) {
temp2[i] = func2(temp1[i]);
}
for(i = 0; i < source.length; i++) {
temp3[i] = func3(temp2[i]);
}
result = temp3;
當引入了延遲求值后,代碼大致就成這樣的了:
var result = [];
for(var i = 0; i < source.length; i++) {
result[i] = func3(func2(func1(source[i])));
}
減少不必要的中間變量多少會帶來性能上的提升,特別是在數據源特別巨大,內存又吃緊的情況下。
延遲執行
延遲求值帶來的另一個好處是延遲執行。無論何時你寫了段鏈式代碼,只有在顯式地調用了.value()
后才會真正執行。這樣一來,在數據源需要異步去拉取的情況下,可以保證我們處理的是最新的數據。
var wallet = _(assets).filter(ownedBy('me'))
.pluck('value')
.reduce(sum);
$json.get("/new/assets").success(function(data) {
assets.push.apply(assets, data); // 更新數據源
wallet.value(); // 返回的結果是最新的
});
而且這種機制在某些情況下也會提高執行效果。我們可以老早發送一個請求獲取數據,然后指定一個精確的時間來執行。
后記
延遲求值并且不算什么新技術。在一些庫中已經在使用了,比如LINQ,Lazy.js還有其他等等。那么問題來了,Lo-Dash
存在的意義是啥?我想就是你仍然可以使用你熟悉的Underscore
接口但享受一個更高效的底層實現,不需要額外的學習成本,代碼上面也不會有大的變動,只需稍加修改。
文章列表