由于 JS 采用 IEEE 754 双精度版本(64位),而且只要采用 IEEE 754 的语言都有该问题。node
咱们都知道计算机表示十进制是采用二进制表示的,因此 0.1
在二进制表示为ajax
// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
复制代码
那么如何获得这个二进制的呢,咱们能够来演算下promise
小数算二进制和整数不一样。乘法计算时,只计算小数位,整数位用做每一位的二进制,而且获得的第一位为最高位。因此咱们得出 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
bash
下面说一下原生解决办法,以下代码所示多线程
parseFloat((0.1 + 0.2).toFixed(10))
复制代码
这个问题相信不少人会第一时间想到 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);
});
复制代码
设计思路以下:函数
如下是代码实现,实现了思路,可是可能会存在 Bug,可是这种设计题通常是给出设计思路和部分代码,不会须要写出一个无问题的代码oop
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;
}
}
复制代码
众所周知 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 顺序是这样的
经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中。
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
复制代码
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的时间并非准确时间,而是在达到这个时间后尽快执行回调,可能会由于系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647]
,若是设定的时间不在这个范围,将被设置为1。
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare 阶段内部实现
poll 阶段很重要,这一阶段中,系统会作两件事情
而且当 poll 中没有定时器的状况下,会发现如下两件事情
setImmediate
须要执行,poll 阶段会中止而且进入到 check 阶段执行 setImmediate
setImmediate
须要执行,会等待回调被加入到队列中并当即执行回调若是有别的定时器须要被执行,会回到 timer 阶段执行回调。
check 阶段执行 setImmediate
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
复制代码
最后附上个人公众号