1、单线程的JavaScript
我们都知道,js是一门单线程语言,何为单线程?就是在同一时间,只能做一件事
为什么js要这么设计呢?js的主要用途就是操作DOM,与用户进行操作,所以如果js有两个线程,这时一个线程在某个节点上修改内容,另一个线程也在该节点上修改该内容,那js要以谁为准呢?
所以js的单线程当然是为了高效安全
为了提高利用多核CPU的计算能力,HTML5提出Web Worker标准,允许js脚本创建多个线程,但是子线程完全受主线程控制且不得操作DOM。所以这个新标准并没有改变js单线程的本质
2、同步任务和异步任务
js的单线程就意味着所有任务都需要排队,前一个任务结束,才会执行下一个任务。但是IO设备很慢,当需要读取数据时,这时候CPU
就会停下来等待IO
操作,更要命的是即使该CPU
再忙,其它CPU
也不会帮忙,大家你看我我看你,这就特别影响用户体验了
所以为了解决阻塞式IO
带来的不好的体验,js规定了,这时候主线程完全可以不管IO
设备,将其挂起处于等待中的任务,然后继续运行后面的任务,等到IO设备运行结果出来后,再回过头来,把挂起的任务继续执行下去。这就是异步操作。
于是,所有的任务可以分成两种,一种是同步任务,一种是异步任务
- 同步任务:在主线程上执行任务,这个前一个任务执行完之后,才能执行下一个任务;如果前一个任务没有执行完,那么线程会一直等待下去,直到该任务执行完才会继续执行
- 异步任务:任务不进入主线程,而是进入“消息队列”,主线程不会一直等待下去,而是继续执行下面的任务,当只有消息队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
现在我们来看一下异步任务的执行机制:
- 所有同步任务都在主线程上执行,形成一个 执行栈
- 主线程之外,还存在一个
消息队列
,只要异步任务有了运行结果,就在消息队列
中放置一个事件,并通知主线程 - 一旦
执行栈
中的所有同步任务执行完毕,系统就会读取消息队列
,相应的事件就结束了等待的状态,进入主线程,开始执行
主线程会不断的执行上面的三个步骤,只要主线程空了,就会去读取 消息队列
,这就是 JavaScript的执行机制
3、消息队列和事件循环
消息队列就是队列,也是遵循先进先出的原则。IO
线程每完成一项任务,就会将该任务添加到消息队列中
所以先进入的任务会优先被主线程读取,只要执行栈一清空,即同步任务已执行完毕,消息队列中的任务就会依次进入主线程。但是有一种特殊情况,那就是定时器,定时器时间没到,是不会被添加到主线程的
现在我们知道异步操作后消息队列会通知主线程,可以来取事件执行了,那么问题来了,这个通知机制是怎么实现的呢?
答案就是事件循环
事件循环(Event Loop
):事件循环是指主线程重复从消息队列中取消息、执行消息的过程
而这里的事件就是我们熟悉的回调函数,该回调函数是在注册异步任务的时候添加的
所以,工作线程将事件添加到消息队列中,主线程通过事件循环去读取事件。而实际上,主线程只会做一件事,就是从任务对列中读取消息、执行消息,再读取、再执行,直到消息队列为空。并且每次主线程只有在将当前的消息执行完毕之后,才会去取下一个消息
下面我们用一张图来更好的表示这个过程:
主线程在运行的时候,会产生堆(heap)和 栈(stack),栈中的代码会调用外部的API,它们在 消息队列
中加入各种事件,只要栈中的代码执行完毕,主线程就会去读取 消息队列
,依次执行那些事件所对应的回调函数
4、定时器
我们先来看一下同步回调
function callback() {
console.log('我是同步回调');
}
function bar(fn) {
console.log(123);
fn();
console.log(456);
}
bar(callback);
// 123
// 我是同步回调
// 456
复制代码
callback
函数作为参数传给了bar
函数,在bar
函数中的callback
就是回调函数,而且是同步回调
我们再来看看异步回调的例子:
function foo() {
console.log('我是异步回调');
}
function bar(fn) {
console.log(123);
setTimeout(fn, 1000);
console.log(456);
}
bar(foo);
// 123
// 456
// 我是异步回调
复制代码
setTimeout
在bar
函数执行结束后延时1s后再执行,这种回调函数在主函数外部执行的过程就称为异步回调
显然,setTimeout()
定时器是一个异步任务,系统会先执行执行栈中的同步任务,再回过头来执行 消息队列
中的事件
即使定时器的延时时间为0
function foo() {
console.log('我是异步回调');
}
function bar(fn) {
console.log(123);
setTimeout(fn, 0);
console.log(456);
}
bar(foo);
// 123
// 456
// 我是异步回调
复制代码
因为setTimeout
本质就是异步任务,无论如何它都会被挂起,js先执行同步任务后,发现消息队列中的任务可以执行了(setTimeout
延时时间到),就再去执行它
值得注意的是:
- 定时器事件虽然是添加到
任务队列
中了,但是也得等它定时完成之后,才会去指定它 - 如果此时它已经位于队列的首位了,但是定时时间还未结束,此时,它也不会被执行,后面事件会先执行
另外,异步回调是指回调函数函数在主函数外部执行,一般有两种方式:
- 第一种:把异步任务添加到消息队列尾部
- 第二种:把异步任务添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了