JS是单线程语言,为什么要一开始设计成单线程语言呢???
JS这门语言设计之初,一 是用来做 页面脚本,也就是客户端的一些简单交互,比如:表单验证这样的事情,二是用来操作DOM,如果JS开两个线程做事情,如果碰见两个线程在操作同一个DOM,那么,浏览器就会迷惑到底要相信哪一个线程的话。所以,为了避免未来JS发展太过于冗杂,JS从诞生以来就沿用单线程的底层设计在发展,并一直沿用至今。
同步和异步和任务队列:
正因为JS是单线程语言,所以同步和异步的概念就格外重要。JS所有的执行任务分为两种,一种是同步任务,一种是异步任务。 可以粗略地认为:同步任务执行在 执行栈上,异步任务放在任务队列中。 我们所写的按顺序普通执行的执行函数都放在主线程执行栈中。 所有的事件回调(例如:点击事件),setTimeout,setInterVal,Promise.then(()=>{})还有 $http()请求,都是异步任务,放置在执行队列中等待被执行。 所以JS的Event Loop机制如下: (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 (2)执行栈之外,还存在一个"任务队列"。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 (4)主线程不断重复上面的第三步。 然而,还没完的是, 上面所说的任务队列并非那么简单,任务队列之中还会细化为两种模式:MacroTasks和MicroTasks。 通常地: MacroTasks包括有:
鼠标,键盘等回调事件
setTimeout
setInterval
setImmediate
requestAnimationFrame I/O UI rendering
MicroTasks包括有:
process.nextTick
Promise.then().then().then()
MVVM框架例如vue的Object.observe等等、
JS的事件循环运行机制就可以看做一个 执行栈+无限个任务队列(MacroTasks) 所构成。而MicroTasks就是附着在 主线程或者每一个 任务队列(MacroTasks)末尾的 小任务队列。每一次任务队列中的任务执行完后,JS就会去检测当前附着的MicroTasks里面有没有任务,有就执行MicroTasks里的任务,没有,就执行下一个大的任务队列(MacroTasks)里的任务。依次循环。直至终结。
面试经典题:
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
复制代码
执行顺序:2,3,5,4,1 1.由于setTimeout 定时函数 定义出来,首先就被放置在下一个任务队列(MacroTasks)中,所以即便第二个参数设置是0,也会延迟到主线程内所有任务执行完后再执行。 2.碰到New Promise()函数,Promise函数内部是按正常流程的主线程中执行的,所以首先输出2。for循环依旧是在主线程里执行,按正常执行,然后碰到输出3,则再正常输出3。 3.碰到then()方法,由于then()内部是放在任务队列中,由于主线程还未执行完,所以then()里的执行被搁置。 4.输出5在主线程中,然后输出5,当前首个任务队列在执行栈中执行完毕。 5.主线程跑完了,接下来就是重点。由上面所说,Promise属性MicroTasks,所以在当前任务队列执行完后,进去MicroTasks队列去看看有没有Promise.then()之类MicroTasks,发现有,则执行输出4。 6.最后,当前首个任务队列执行完了,附着在任务队列最后的MicroTasks队列也执行完了,就可以执行下一个 任务队列(MacroTasks),即一开始被setTimeout放置的console.log(1),即最后 输出1。
经典例题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
复制代码
由于for循环内是一个 压入任务队列(MacroTasks)的setTimeout异步函数,所以for循环内部并没有去执行那setTimeout内部的匿名回调函数,但确实执行了第二个参数1000*i,定义了然后执行到了下一个 任务队列(MacroTasks),然后进入到setTimeout的回调函数中去,由于此时,对于回调的匿名函数内部来说,i已经累加到5了,所以,i=5是一个定值。所以会持续输出五个5,由于第二个参数确实在for循环中执行了,所以是在第0秒,第1秒,第2秒,第3秒,第4秒期间持续输入的五个5。
那如何改成 正常地输出 0,1,2,3,4呢?
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
复制代码
或者: var i 改成let i
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
复制代码
通过立即执行函数或 var 改成let,来自己构成一个封闭的作用域, 原理是: 让i 处在 运行的时候当前的作用域,而非 i处在定义的时候i当前的作用域