目录
为什么js是单线程的
JavaScript是单线程的。单线程在程序执行时,所走的程序路径按照顺序排下来,前面的必须处理好,后面的才会执行。也就是说,同一时间只能做一件事。我们都知道单线程的效率不如多线程效率高,例如做饭的时候,我们需要炒菜还煮粥,如果是一个人就像单线程一样,需要先完成炒菜的全部工作,洗菜、切菜、在炒菜。全部完成以后在淘米、烧水、煮粥。如果我们找一个人帮忙,就会相当于又开了一个线程,一条线程洗菜、切菜、炒菜,另一条线程淘米、烧水、煮粥。由此可见多线程一定是比单线程更快,效率更高的。那么为什么js是单线程的不能有多线程呢。
js的单线程离不开他的用途。js作为一门浏览器端的脚本语言,主要的任务是与用户互动,以及操作dom。如果js是多线程的必定会造成dom资源的争夺。比如,假定JavaScript同时有两个线程,一个线程要求操作某个DOM节点,另一个线程要求删除这个节点,这时浏览器应该以哪个线程为准?
为什么js需要异步
虽然单线程避免了DOM资源争夺的问题,但是我们知道单线程有着不可避免的阻塞问题。在单线程中一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成再执行后面一个任务。那么如果有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。我们可以尝试写一个死循环的函数,并调用就会发现浏览器直接无响应。就是因为当执行到死循环函数时,该函数一直执行,导致页面卡在这个地方,其他任务无法执行。
function fun(param){
while(true){
console.log(param++);
}
}
fun(0);
我们在正常业务中写代码肯定不会把函数写成死循环,但是比如我们写一个定时器,或者向服务器请求数据时。如果没有异步,在单纯的单线程中,定时器等待两秒页面就卡顿2秒,网络不好请求数据请求了半分钟页面就卡顿半分钟,这样的用户体验一定是极其不好的。所以我们就需要异步处理一些需要时间等待的函数。
什么是异步
异步指多个相关事物的发生无需等待其前一事物的完成,异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。
setTimeout是同步执行的还是异步执行的,我们可以使用setTimeout等待10秒看看。
function fun1(){
let now = new Date().getSeconds();
while(true) {
let time = new Date().getSeconds() - now;
if(time >= 3) {
console.log("looped for 10 seconds");
break;
}
}
}
function fun2(){
setTimeout(function(){
console.log("looped for 10 seconds setTimeout");
},10000)
}
fun1();
fun2();
我们会发现通过循环延迟10秒,浏览器就会卡顿10秒,一直等到循环结束才能继续正常操作,如果使用setTimeout延迟10秒执行完全不会影响浏览器正常操作。包括如果先调用fun2在调用fun1会发现,fun2延迟10秒打印成功后,fun2随即执行完毕。说明fun1与fun2是几乎同时执行的,如果按照普通的单线程应该是fun2执行延迟10秒执行结束,然后fun1在执行延迟10秒后执行完毕。由此可见setTimeout与普通函数执行是异步的。
怎么实现异步
- JS分为同步任务和异步任务
- 同步任务都在主线程上执行
- 主线程之外,事件触发线程管理着一个
任务队列
,遇到异步任务挂起至异步线程,只要异步任务有了运行结果,就在任务队列
之中放置一个事件。 - 一旦的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取
任务队列
,将可运行的异步任务添加到可执行栈中,开始执行。
function a(){
b();
c();
console.log('a')
}
function b(){
c();
console.log('b')
}
function c(){
console.log('c')
}
a(); // c b c a
那么什么是任务队列:任务队列是一个先进先出的队列,它里面存放着各种任务(这里存放的就是各种已经被触发的异步任务回调)。
我们知道js是单线程的,所以异步事件究竟是交给谁去处理了,事件触发线程究竟是哪来的?
虽然js是单线程的,但是js是依托于浏览器的,浏览器是多线程的,在浏览器中有专门的异步处理模块。所以当我们在执行js代码时,遇到同步任务直接放到我们的执行栈中执行,遇到异步任务会调用交由浏览器处理。当达到触发条件时会把相应的回调函数放到异步任务队列中,等待调度这就是事件循环机制。
实际上,主线程只会做一件事情,就是从任务队列里面取任务、执行任务,再取任务、再执行。当任务队列为空时,就会等待直到任务队列变成非空。而且主线程只有在将当前的任务执行完成后,才会去取下一个任务。这种机制就叫做事件循环机制,取一个任务并执行的过程叫做一次循环。
$.ajax({
type: "GET",
url: " http://www.baidu.com/",
success: function(data) {
console.log("请求成功");
},
error: function(jqXHR) {
console.log("请求失败");
}
});
let num = 10 * 100;
console.log(num);
主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求百度一下,你就知道,无论成功拿到响应后,然后将对应的回掉函数放到消息队列中。主线程在执行完当前的所有代码后,就会到消息队列取出相应的回调函数,并执行它。到此为止,就完成了工作线程
对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
宏任务与微任务
任务队列又分为宏任务队列与微任务队列。
宏任务
程序初始化、setTimeout
、setInterval
、setImmediate
(浏览器暂时不支持,只有IE10支持,具体可见MDN
)、I/O
、UI Rendering
,dom事件,ajax
微任务
Process.nextTick(Node独有)
、Promise
、Object.observe(废弃)
、MutationObserver
(具体使用方式查看这里)
执行顺序
在一次事件循环中,先执行宏任务中的同步代码,同步代码执行完毕执行栈后会查看微任务队列,如果微任务队列有任务,微任务队列中的每一个任务会依次被执行,它会等到微任务队列为空才会停止执行——即使中途有微任务加入。当微任务队列为空一次事件循环结束,到宏任务队列中查找下一个任务,重复同步代码=>微任务队列的执行。
var promise = new Promise(function(resolve) {
resolve()
})
setTimeout(function() {
console.log("setTimeout")
promise.then(() => {
console.log('promise in')
})
},100)
promise.then(() => {
console.log('promise out')
})
console.log('out'); out promise out setTimeout promise in
零延迟
零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout
并不表示在 0 毫秒后就立即调用回调函数。其等待的时间取决于在调用其回调函数之前其他任务的执行时间。
setTimeout(function() {
console.log("等待0s")
},0)
function fun1() {
let now = new Date().getSeconds();
while (true) {
let time = new Date().getSeconds() - now;
if (time >= 3) {
console.log("looped for 3 seconds");
break;
}
}
}
fun1()
思考题
function a() {
var promise = new Promise(function(resolve) {
resolve()
})
promise.then(() => {
console.log('222222222222')
})
setTimeout(function() {
console.log('3333333333')
},0)
}
function b() {
var promise = new Promise(function(resolve) {
resolve()
})
promise.then(() => {
console.log('1111111111')
})
}
a()
b()
console.log('main1');
let promise = new Promise(function(resolve, reject) {
resolve();
})
promise.then(function() {
console.log('promise then out top');
})
setTimeout(function() {
console.log('setTimeout');
promise.then(function() {
console.log('promise then in');
});
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then out');
});
console.log('main2');
setTimeout(function() {
var promise = new Promise(function(resolve) {
console.log('11111111111')
resolve()
})
promise.then(() => {
console.log('222222222222')
Promise.resolve().then(()=>{
console.log('77777777')
})
}).then(() => {
console.log('6666666666')
})
}, 1000)
setTimeout(function() {
console.log('4444444444');
var promise = new Promise(function(resolve) {
resolve()
})
promise.then(() => {
console.log('555555555555')
})
}, 2000)