JS 之同步操作 vs 异步操作

SegmentFault

共 3301字,需浏览 7分钟

 ·

2021-04-29 20:38

作者:Pingan8787

来源:SegmentFault 思否社区


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设备运行结果出来后,再回过头来,把挂起的任务继续执行下去。这就是异步操作。

于是,所有的任务可以分成两种,一种是同步任务,一种是异步任务

  • 同步任务:在主线程上执行任务,这个前一个任务执行完之后,才能执行下一个任务;如果前一个任务没有执行完,那么线程会一直等待下去,直到该任务执行完才会继续执行
  • 异步任务:任务不进入主线程,而是进入“消息队列”,主线程不会一直等待下去,而是继续执行下面的任务,当只有消息队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

现在我们来看一下异步任务的执行机制:

  1. 所有同步任务都在主线程上执行,形成一个 执行栈

<img src="https://pic4.zhimg.com/80/v2-1778c62c1eaa529a5457eb6839547794_720w.png" alt="img" style="zoom:80%;" />

  1. 主线程之外,还存在一个 消息队列,只要异步任务有了运行结果,就在 消息队列中放置一个事件,并通知主线程
  2. 一旦 执行栈中的所有同步任务执行完毕,系统就会读取 消息队列,相应的事件就结束了等待的状态,进入主线程,开始执行

主线程会不断的执行上面的三个步骤,只要主线程空了,就会去读取 消息队列,这就是 JavaScript的执行机制。

3、消息队列和事件循环

消息队列就是队列,也是遵循先进先出的原则。IO线程每完成一项任务,就会将该任务添加到消息队列中。


所以先进入的任务会优先被主线程读取,只要执行栈一清空,即同步任务已执行完毕,消息队列中的任务就会依次进入主线程。但是有一种特殊情况,那就是定时器,定时器时间没到,是不会被添加到主线程的。
现在我们知道异步操作后消息队列会通知主线程,可以来取事件执行了,那么问题来了,这个通知机制是怎么实现的呢?
答案就是事件循环
事件循环(Event Loop):事件循环是指主线程重复从消息队列中取消息、执行消息的过程。
而这里的事件就是我们熟悉的回调函数,该回调函数是在注册异步任务的时候添加的。
所以,工作线程将事件添加到消息队列中,主线程通过事件循环去读取事件。而实际上,主线程只会做一件事,就是从任务对列中读取消息、执行消息,再读取、再执行,直到消息队列为空。并且每次主线程只有在将当前的消息执行完毕之后,才会去取下一个消息。
下面我们用一张图来更好的表示这个过程:
<img src="https://pic1.zhimg.com/80/v2-651442e7e305c14c118a29e78e700d3d_720w.png" alt="img" style="zoom: 67%;" />
主线程在运行的时候,会产生堆(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
// 我是异步回调
setTimeoutbar函数执行结束后延时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延时时间到),就再去执行它。
值得注意的是:
  • 定时器事件虽然是添加到 任务队列中了,但是也得等它定时完成之后,才会去指定它
  • 如果此时它已经位于队列的首位了,但是定时时间还未结束,此时,它也不会被执行,后面事件会先执行
另外,异步回调是指回调函数函数在主函数外部执行,一般有两种方式:
  1. 第一种:把异步任务添加到消息队列尾部
  2. 第二种:把异步任务添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了


点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -

浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报