异步
前置知识:同步与异步
同步就是,任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。
这有时候会很令人困扰,比如在发送网络请求时,程序会因此停止几百毫秒,这时对用户体验的影响是非常大的,因此 Java 这种语言提供的解决方案就是多线程,程序员可以把网络请求这种会等待很长时间的代码放到一个新线程中,然后主线程该往下跑就跑,这当然是美好的。
但是 JavaScript 是单线程的语言,所以 JavaScript 提供了完全不同的解决方案:异步,异步又分为两派:回调和 Promise。
前置知识:回调与 Promise
回调的典型就是 setTimeout()
了,没学过的我稍微演示一下写法:
setTimeout(() => {
console.log("Hello, World!")
}, 2000)
上面的代码会在大约 2000ms 后输出你好世界,看起来很简单,回调就是这么简洁的东西,就是提前提供一个函数,然后等条件达到的时候执行而已。回调是很简洁的东西,前提是他们没有扎堆。如果不信可以去看看 Callback Hell 上的代码。
为了解决这种回调地域,ES6 提供了 Promise。
事件队列
先看一个示例吧
for (var i = 1; i<= 3; i++) {
setTimeout(() => console.log(i), 1000)
}
// 4 4 4
你以为结果会是每隔一秒输出一次 1 2 3,然而确是在一秒后同时输出了三个 4,所以问题出在了哪?
在 JavaScript 脚本中,主线程运行的时候,产生堆栈,栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件。只要栈中的代码执行完毕,主线程就会去不断读取"任务队列",依次执行那些事件所对应的回调函数。调用 setTimeout()
时,实际上就往任务队列中添加了一个事件。
所以上面的代码实际上执行的顺序是这样的:
- 第一次循环,添加一个 1 秒后输出 i 的事件,此时 i 是 2
- 第二次循环,添加一个 1 秒后输出 i 的事件,此时 i 是 3
- 第三次循环,添加一个 1 秒后输出 i 的事件,此时 i 是 4
- 代码执行完了,JavaScript 虚拟机就一直盯着队列
- 依次执行队列里的三个输出 i,实际上此时 i 是 4,所以输出了三个 4
这就是事件队列,很简洁,但是若是不知道这个机制,就会感到非常困扰。
因为 JavaScript 虚拟机会在执行栈里的所有代码之后才会开始监测队列里的延时时间是否需要触发,因此 setTimeout()
并不能保证在确定的时间执行代码。
知道了这些,我们再修复上面的代码,我们把 setTimeout()
的第二个参数设置为 i * 1000:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), i * 1000)
}
// 4 4 4
会形成这样的队列:
console.log(i)
1000msconsole.log(i)
2000msconsole.log(i)
3000ms
最后输出 i,虽然这回是一秒输出一次了,但是输出的结果还是 4,这就是 var 的变量提升问题了,我们换成 let 就能解决:
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), i * 1000)
}
// 1 2 3
昨日份乐子
东半球最先进的排序算法,时间复杂度总是 $O(n)$
[3, 7, 2, 1].forEach(item => setTimeout(() => console.log(item), item))