爲何 0.1 + 0.2 != 0.3,請詳述理由
由於 JS 採用 IEEE 754 雙精度版本(64位),而且只要採用 IEEE 754 的語言都有該問題。html
咱們都知道計算機表示十進制是採用二進制表示的,因此 0.1
在二進制表示爲node
// (0011) 表示循環 0.1 = 2^-4 * 1.10011(0011) 複製代碼
那麼如何獲得這個二進制的呢,咱們能夠來演算下git
小數算二進制和整數不一樣。乘法計算時,只計算小數位,整數位用做每一位的二進制,而且獲得的第一位爲最高位。因此咱們得出 0.1 = 2^-4 * 1.10011(0011)
,那麼 0.2
的演算也基本如上所示,只須要去掉第一步乘法,因此得出 0.2 = 2^-3 * 1.10011(0011)
。github
回來繼續說 IEEE 754 雙精度。六十四位中符號位佔一位,整數位佔十一位,其他五十二位都爲小數位。由於 0.1
和 0.2
都是無限循環的二進制了,因此在小數位末尾處須要判斷是否進位(就和十進制的四捨五入同樣)。ajax
因此 2^-4 * 1.10011...001
進位後就變成了 2^-4 * 1.10011(0011 * 12次)010
。那麼把這兩個二進制加起來會得出 2^-2 * 1.0011(0011 * 11次)0100
, 這個值算成十進制就是 0.30000000000000004
promise
下面說一下原生解決辦法,以下代碼所示瀏覽器
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
1.若是以爲這篇文章還不錯,來個分享、點贊吧,讓更多的人也看到