為什麼 0.1 + 0.2 != 0.3,請詳述理由
因為 JS 採用 IEEE 754 雙精度版本(64位),並且只要採用 IEEE 754 的語言都有該問題。
我們都知道計算機表示十進位制是採用二進位制表示的,所以 0.1
在二進位制表示為
// (0011) 表示迴圈
0.1 = 2^-4 * 1.10011(0011)
複製程式碼
那麼如何得到這個二進位制的呢,我們可以來演算下
小數算二進位制和整數不同。乘法計算時,只計算小數位,整數位用作每一位的二進位制,並且得到的第一位為最高位。所以我們得出 0.1 = 2^-4 * 1.10011(0011)
,那麼 0.2
的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)
。
回來繼續說 IEEE 754 雙精度。六十四位中符號位佔一位,整數位佔十一位,其餘五十二位都為小數位。因為 0.1
和 0.2
都是無限迴圈的二進位制了,所以在小數位末尾處需要判斷是否進位(就和十進位制的四捨五入一樣)。
所以 2^-4 * 1.10011...001
進位後就變成了 2^-4 * 1.10011(0011 * 12次)010
。那麼把這兩個二進位制加起來會得出 2^-2 * 1.0011(0011 * 11次)0100
, 這個值算成十進位制就是 0.30000000000000004
下面說一下原生解決辦法,如下程式碼所示
parseFloat((0.1 + 0.2).toFixed(10))
複製程式碼
10 個 Ajax 同時發起請求,全部返回展示結果,並且至多允許三次失敗,說出設計思路
這個問題相信很多人會第一時間想到 Promise.all
,但是這個函式有一個侷限在於如果失敗一次就返回了,直接這樣實現會有點問題,需要變通下。以下是兩種實現思路
// 以下是不完整程式碼,著重於思路 非 Promise 寫法
let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {
if (success) {
success++
if (success + errorCount === 10) {
console.log(datas)
} else {
datas.push(res.data)
}
} else {
errorCount++
if (errorCount > 3) {
// 失敗次數大於3次就應該報錯了
throw Error('失敗三次')
}
}
})
// Promise 寫法
let errorCount = 0
let p = new Promise((resolve, reject) => {
if (success) {
resolve(res.data)
} else {
errorCount++
if (errorCount > 3) {
// 失敗次數大於3次就應該報錯了
reject(error)
} else {
resolve(error)
}
}
})
Promise.all([p]).then(v => {
console.log(v);
});
複製程式碼
基於 Localstorage 設計一個 1M 的快取系統,需要實現快取淘汰機制
設計思路如下:
- 儲存的每個物件需要新增兩個屬性:分別是過期時間和儲存時間。
- 利用一個屬性儲存系統中目前所佔空間大小,每次儲存都增加該屬性。當該屬性值大於 1M 時,需要按照時間排序系統中的資料,刪除一定量的資料保證能夠儲存下目前需要儲存的資料。
- 每次取資料時,需要判斷該快取資料是否過期,如果過期就刪除。
以下是程式碼實現,實現了思路,但是可能會存在 Bug,但是這種設計題一般是給出設計思路和部分程式碼,不會需要寫出一個無問題的程式碼
class Store {
constructor() {
let store = localStorage.getItem('cache')
if (!store) {
store = {
maxSize: 1024 * 1024,
size: 0
}
this.store = store
} else {
this.store = JSON.parse(store)
}
}
set(key, value, expire) {
this.store[key] = {
date: Date.now(),
expire,
value
}
let size = this.sizeOf(JSON.stringify(this.store[key]))
if (this.store.maxSize < size + this.store.size) {
console.log('超了-----------');
var keys = Object.keys(this.store);
// 時間排序
keys = keys.sort((a, b) => {
let item1 = this.store[a], item2 = this.store[b];
return item2.date - item1.date;
});
while (size + this.store.size > this.store.maxSize) {
let index = keys[keys.length - 1]
this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
delete this.store[index]
}
}
this.store.size += size
localStorage.setItem('cache', JSON.stringify(this.store))
}
get(key) {
let d = this.store[key]
if (!d) {
console.log('找不到該屬性');
return
}
if (d.expire > Date.now) {
console.log('過期刪除');
delete this.store[key]
localStorage.setItem('cache', JSON.stringify(this.store))
} else {
return d.value
}
}
sizeOf(str, charset) {
var total = 0,
charCode,
i,
len;
charset = charset ? charset.toLowerCase() : '';
if (charset === 'utf-16' || charset === 'utf16') {
for (i = 0, len = str.length; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode <= 0xffff) {
total += 2;
} else {
total += 4;
}
}
} else {
for (i = 0, len = str.length; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode <= 0x007f) {
total += 1;
} else if (charCode <= 0x07ff) {
total += 2;
} else if (charCode <= 0xffff) {
total += 3;
} else {
total += 4;
}
}
}
return total;
}
}
複製程式碼
詳細說明 Event loop
眾所周知 JS 是門非阻塞單執行緒語言,因為在最初 JS 就是為了和瀏覽器互動而誕生的。如果 JS 是門多執行緒的語言話,我們在多個執行緒中處理 DOM 就可能會發生問題(一個執行緒中新加節點,另一個執行緒中刪除節點),當然可以引入讀寫鎖解決這個問題。
JS 在執行的過程中會產生執行環境,這些執行環境會被順序的加入到執行棧中。如果遇到非同步的程式碼,會被掛起並加入到 Task(有多種 task) 佇列中。一旦執行棧為空,Event Loop 就會從 Task 佇列中拿出需要執行的程式碼並放入執行棧中執行,所以本質上來說 JS 中的非同步還是同步行為。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
複製程式碼
以上程式碼雖然 setTimeout
延時為 0,其實還是非同步。這是因為 HTML5 標準規定這個函式第二個引數不得小於 4 毫秒,不足會自動增加。所以 setTimeout
還是會在 script end
之後列印。
不同的任務源會被分配到不同的 Task 佇列中,任務源可以分為 微任務(microtask) 和 巨集任務(macrotask)。在 ES6 規範中,microtask 稱為 jobs
,macrotask 稱為 task
。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
複製程式碼
以上程式碼雖然 setTimeout
寫在 Promise
之前,但是因為 Promise
屬於微任務而 setTimeout
屬於巨集任務,所以會有以上的列印。
微任務包括 process.nextTick
,promise
,Object.observe
,MutationObserver
巨集任務包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
很多人有個誤區,認為微任務快於巨集任務,其實是錯誤的。因為巨集任務中包括了 script
,瀏覽器會先執行一個巨集任務,接下來有非同步程式碼的話就先執行微任務。
所以正確的一次 Event loop 順序是這樣的
- 執行同步程式碼,這屬於巨集任務
- 執行棧為空,查詢是否有微任務需要執行
- 執行所有微任務
- 必要的話渲染 UI
- 然後開始下一輪 Event loop,執行巨集任務中的非同步程式碼
通過上述的 Event loop 順序可知,如果巨集任務中的非同步程式碼有大量的計算並且需要操作 DOM 的話,為了更快的 介面響應,我們可以把操作 DOM 放入微任務中。
Node 中的 Event loop
Node 中的 Event loop 和瀏覽器中的不相同。
Node 的 Event loop 分為6個階段,它們會按照順序反覆執行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製程式碼
timer
timers 階段會執行 setTimeout
和 setInterval
一個 timer
指定的時間並不是準確時間,而是在達到這個時間後儘快執行回撥,可能會因為系統正在執行別的事務而延遲。
下限的時間有一個範圍:[1, 2147483647]
,如果設定的時間不在這個範圍,將被設定為1。
I/O
I/O 階段會執行除了 close 事件,定時器和 setImmediate
的回撥
idle, prepare
idle, prepare 階段內部實現
poll
poll 階段很重要,這一階段中,系統會做兩件事情
- 執行到點的定時器
- 執行 poll 佇列中的事件
並且當 poll 中沒有定時器的情況下,會發現以下兩件事情
- 如果 poll 佇列不為空,會遍歷回撥佇列並同步執行,直到佇列為空或者系統限制
- 如果 poll 佇列為空,會有兩件事發生
- 如果有
setImmediate
需要執行,poll 階段會停止並且進入到 check 階段執行setImmediate
- 如果沒有
setImmediate
需要執行,會等待回撥被加入到佇列中並立即執行回撥
- 如果有
如果有別的定時器需要被執行,會回到 timer 階段執行回撥。
check
check 階段執行 setImmediate
close callbacks
close callbacks 階段執行 close 事件
並且在 Node 中,有些情況下的定時器執行順序是隨機的
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 這裡可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於效能
// 因為可能進入 event loop 用了不到 1 毫秒,這時候會執行 setImmediate
// 否則會執行 setTimeout
複製程式碼
當然在這種情況下,執行順序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 因為 readFile 的回撥在 poll 中執行
// 發現有 setImmediate ,所以會立即跳到 check 階段執行回撥
// 再去 timer 階段執行 setTimeout
// 所以以上輸出一定是 setImmediate,setTimeout
複製程式碼
上面介紹的都是 macrotask 的執行情況,microtask 會在以上每個階段完成後立即執行。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上程式碼在瀏覽器和 node 中列印情況是不同的
// 瀏覽器中列印 timer1, promise1, timer2, promise2
// node 中列印 timer1, timer2, promise1, promise2
複製程式碼
Node 中的 process.nextTick
會先於其他 microtask 執行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
複製程式碼
最後附上我的公眾號