JS同步和异步、事件循环

1,851 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

同步任务与异步任务

简单介绍

JS是单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
这是因为Javascript这门脚本语言诞生的使命所致——javaScript是处理页面中用户的交互,以及操作DOM而诞生的。
比如我们对某个DOM元素进行添加和删除操作,不能同时进行。应该先进行添加,之后再删除。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

来看看第一个问题

        console.log(1);
        setTimeout(function () {
            console.log(3);
        }, 1000);
        console.log(2);

虽然JS原先是一个单线程,而后来提出了异步的概念,允许多个任务进行,所以这个程序输出1,2,3

为了解决单线程问题,利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程。于是,JS中出现了同步和异步。

同步: 前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10min后),再去切菜,炒菜。

异步: 你在做一件事情时,因为这件事情会花费很长时间,在做这件事情的同时,你还可以去处理其他事情。比如做饭的异步做法,我们在烧水的同时,利用这10min去切菜和炒菜。

来看看第二个问题

        console.log(1);
        setTimeout(function () {
            console.log(3);
        }, 0); 
        console.log(2);

如果把刚刚第一个问题的定时器的延时改为0,那么输出结果会变吗?其实结果还是1,2,3

原因是,在JS中,为了避免阻塞和等待,把任务分为了两大类,即同步任务异步任务
JS的同步任务呢,放在了主线程上执行,形成执行栈
JS的异步任务是通过回调函数实现的,一般而言,异步任务有以下三种类型:普通事件click、resize等,资源加载load、error等,定时器包括setTimeout、setInterval等
而异步任务相关回调函数放在了任务队列中(或者叫消息队列)


同步异步任务执行顺序图

image.png
如上图,JS执行机制:
先执行了栈中的同步任务,如上图左边,左边按顺序执行,执行到了setTimeout了,也就是遇到了回调函数,那么JS会先把回调函数放入任务队列中。放完了之后呢,再执行下一个同步任务,也就是输出了2。
这时栈中所有同步任务已经完成,系统就会依次序读取任务队列中的异步任务,被读取的异步任务结束等待状态,进入执行栈,开始执行。
①:先执行执行栈中的同步任务
②:异步任务(回调函数)放入任务队列中
③:一旦执行栈中的所有同步任务执行完毕,系统就会按顺序读取任务队列中的异步任务,被读取的异步任务结束等待进入执行栈中执行

事件循环

image.png 事件循环:由于主线程不断地获取任务、执行任务、再获取,所以这种机制被称为事件循环

刚刚我们说,JS在执行的时候,先执行同步任务,遇到了异步任务就把异步任务放入消息队列中,继续执行其他同步任务,直到所有同步任务执行完再来按次序执行异步任务。

那么有这种情况,我的异步任务不止一个,我可能有很多个,难道每一个都要放入消息队列里面吗?
这时候就需要请一个帮手,它就是异步进程处理

看看图中的程序(点击事件的fn是输出"click",定时器的fn是输出3)
①: 图中的程序先打印了1
②: 然后执行第二句话,遇到了回调函数fn,这时候系统把这句话提交给异步进程处理,由它来决定你是否能进消息队列,而异步进程处理在等你触发点击事件,只有你点击了,它才会把这个回调函数fn放入消息队列,你不点击,它是不会写到消息队列里面的。
③: 接着继续执行,打印了2。
④: 然后又读取到了异步任务 ———— 一个延迟3秒的定时器,系统又把它提交给异步进程处理,它会等你的3秒到了,才会把你的回调函数放入消息队列中。放进去归放进去,这个定时器的回调函数是不会执行的,因为要等待所有的同步任务执行完,才会去依次执行消息队列中的异步任务。
这时候主线程执行栈的同步任务其实已经执行完了,系统就会回到消息队列中将刚刚放入的定时器的回调函数送入主线程执行栈中执行(输出3)。

如果你有点击鼠标,异步进程处理就会把点击事件中的回调函数放入消息队列中,此时虽然同步任务已经运行完了,但系统还是会回到消息队列中看看有没有新的异步任务出现,刚刚的定时器的回调函数已经执行完毕了,这时候消息队列里面是空的,所以直接就把你触发的点击事件的回调函数送入了主线程执行栈中处理,输出click

如果此时你又点击了,异步进程处理又把这个任务放到了消息队列中,系统又去消息队列中瞅一瞅,如果有异步任务,就会再拿到主线程执行栈中执行。再点击再执行,这个反复的过程,称为事件循环

后续补充

当时不知道还有宏任务微任务这一回事,现在补充一下。
问: "说说事件循环Event loop"
答:
因为JS是单线程的,为了防止某些代码执行时间过长,JS的任务分成了同步任务和异步任务。
系统会先将遇到的同步任务压入主线程执行栈中,马上执行。
将遇到的异步任务推入异步队列,而异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。
等主线程执行栈中所有的同步代码执行完之后,系统会先去执行清空微任务队列,之后就会进入宏任务队列取队列第一项放入主线程执行栈,执行宏任务中的代码。期间如果遇到同步代码还是先压入主线程执行栈马上执行,遇到宏任务也是推入宏任务队列,遇到微任务也是推入微任务队列,当本次宏任务执行完之后 系统会重复刚刚的步骤,去清空微任务队列,取宏任务队列第一项入主线程执行栈,这个过程就叫事件循环。还有,主线程执行栈好像也算是宏任务