资讯专栏INFORMATION COLUMN

JS专题之事件循环

mengera88 / 2245人阅读

摘要:宏任务需要多次事件循环才能执行完。总结事件循环是和事件调用机制的核心,保证了页面可以有序无阻塞的进行。事件循环的主要逻辑是先执行调用栈,直到清空调用栈只剩下全局上下文。微任务执行后完,进行页面渲染和垃圾回收后进行下一轮事件循环。

准备知识
1. 进程(process)

进程是系统资源分配一个独立单位,一个程序至少有一个进程。比方说:一个工厂代表一个 CPU, 一个车间就是一个进程,任一时刻,只能有一个进程在运行,其他进程处于非运行状态。

2. 线程(Thread)

线程是CPU调度和分派的基本单位,一个线程只能属于一个进程,一个进程可以有多个线程且至少有一个。比方说一个车间的工人,可以有多个工人一起工作。

生活中常常能看到,某某电脑 CPU 的 4 核 4 线程,其意思是指,这款 CPU 同一时间最多只能运行 4 个线程,所以有些线程会处于工作状态,有的线程会处于中断,堵塞,睡眠状态。

经常看到有很多任务同时在进行,一边工作,一边听歌,还一边下载电影。那是因为这些线程在以闪电般的速度不断的切换主要的几个线程,所以,人的体验上感觉是很多很多任务在同时进行。

3. 栈(stack)

栈是一种数据结构,具有后进先出的特点,最开始进入栈结构的数据反而最后才能出来。

4. 队列(queue)

队列也是一种数据结构,数据只能从一边进,一边出,先进去的自然就先出来。

5. 同步和异步(sync async)

同步和异步关注的消息通信机制,同步在函数调用时,如果调用者没有拿到响应结果,程序会继续等待,知道拿到结果为止。而异步会执行其后的代码,等到有响应结果后,才处理响应。

6. 阻塞和非阻塞(blocking & non-blocking)

阻塞和非阻塞关注的是程序等待调用结果时的状态,阻塞的意思是,在调用结果返回响应前,线程会被挂起占用,程序无法继续往下走,而非阻塞的线程则不会挂起,后面的代码能够继续往下执行。

比方说:我去超市买包薯片,老板告诉我货架上没货了,马上去库房拿,这过程中,老板要我站着等他,直到他拿到货出来给我。这个过程就是阻塞。

如果老板告诉我,可以先回去,他一会去库房拿,拿到了之后打电话给我。这个过程,就是非阻塞的,我不用等待,还可以干其他的事情。

7. 执行栈(execution stack)

js 代码在执行代码时,JS 会给调用代码生成一个执行上下文对象,并将其压入执行上下文栈,首先进入栈底的是全局上下文,然后是函数的执行上下文(Execution Context),函数执行完之后,函数上下文从栈中弹出,直到退出浏览器,全局上下文才从栈底弹出。

用代码举个例子:

var globalName = "window";

var foo1 = function() {
    console.log("foo1");
}

var foo2 = function() {
    console.log("foo2");
    foo1();
}

foo2();

上面的图片大致能够描述执行上下文栈的实现逻辑,有关执行上下文的知识,大家可以翻看我之前的文章 - 《JavaScript 之执行上下文》

二、为什么 JS 是单线程模型?
JavaScript 的一个非常有趣的特性是事件循环模型,与许多其他语言不同,它永不阻塞。 处理 I/O 通常通过事件和回调来执行  -- MDN

浏览器主要任务是给用户是视觉和交互上的体验,如果页面使用过程中,偶尔出现阻塞、挂起、无响应的体验一定是非常糟糕的。同时,如果采用多线程同步的模型,那么如何保证同一时间修改了 DOM, 到底是哪个线程先生效呢。

浏览器执行环境的核心思想在于任务调度方式的特别:

哪个任务的优先级高,先来就先运行,直到执行完了才执行下一个,并且同一时刻只能执行一个代码片段,即所谓的单线程模型。

比方说,银行的柜台只开启了一个柜台,每个人想要办理业务,就得先拿号排队,叫到了你的号码,你才能上去办理业务。不能多个人同时在一个柜台办理业务,不然就很容易出差错。

三、事件循环

事件循环是 JS 处理各种事件的核心,由于多个线程同时操作 DOM, 造成不可控的问题,所以 JS 采用了单线程模型。另外,由于所有的事件同步执行,执行完一个才能执行下一个,会造成页面渲染的堵塞。JS 中存在异步事件,用户可以在点击页面的时候,请求网络响应的同事,还可以进行其他的点击操作,保证了页面不会因为网络请求,多种 IO 接口响应慢造成代码执行的堵塞和挂起。

事件循环的顺序是:

进入 script 标签,创建全局上下文

执行全局上下文中的函数,将其压入执行调用栈

某个函数执行完后,函数弹出执行栈,清空函数上下文中的变量对象和内存空间,判断是否需要更新渲染,如果需要则更新渲染。

如果遇到异步事件,也会压入执行调用栈,但浏览器识别到它是异步事件后,会将其弹出执行栈,然后将异步事件的回调函数放入事件队列中。

执行直到函数调用栈清空只剩全局执行上下文,这时,JS 会检查事件队列中是否有事件,如果有,则将事件队列中的一个事件出队,然后压入执行栈中执行。

当执行栈又清空只剩全局执行上下文时,又会重复第 5 步。这就是 JS 的事件循环。

当用户关闭浏览器,全局执行上下文弹出执行栈,清空相应上下文中的变量对象和内存空间。

接下来我们用代码来解释:

console.log("script start!");

function foo1() {
    console.log("foo1");
}

foo1();

setTimeout(function () {
    console.log("setTimeout!");
}, 1000);

function foo2() {
    console.log("foo2");
}

foo2();

console.log("script end!");

打印:
// script start!
// foo1
// foo2
// script end!

// setTimeout!

那我们尝试把 setTimeout 的延迟时间改为 0,想要立即执行,看会不会立即执行:

console.log("script start!");

function foo1() {
    console.log("foo1");
}

foo1();

setTimeout(function () {
    console.log("setTimeout!");
}, 0);

function foo2() {
    console.log("foo2");
}

foo2();

console.log("script end!");

打印:
// script start!
// foo1
// foo2
// script end!
// setTimeout!

可以看出 setTimeout 属于异步事件,总是会在主线程的任务执行完后才开始执行。

顺便说一下事件循环几个原则:

一次只处理一个任务

一个任务从开始到完成,不会被其他任务所中断

这两个原则保证了浏览器任务单元的完整性,事件调用的有序性。

四、宏任务和微任务

事件循环的实现本来应该由一个用于宏任务的队列和一个用于微任务的队列进行完成,这使得事件循环要根据任务类型来进行优先处理。

宏任务:
宏任务包括:

创建文档对象、解析 HTML、执行主线程代码(script)

执行各种事件:页面加载、输入、点击

setTimout,setInterval 异步事件

宏任务代表一个个离散、独立的工作单元,运行完任务后,浏览器可以进行其他的任务调度,如更新渲染或执行垃圾回收。宏任务需要多次事件循环才能执行完。

微任务:
微任务包括:

Promise 回调函数

new MutaionObserver()

微任务是更小的任务,微任务需要尽可能地、通过异步方式执行,微任务更新浏览器的状态,但必须在浏览器执行其他任务之前执行。微任务使得我们避免不必要的 UI 重绘。微任务在一次事件循环中必须全部执行完。

宏任务和微任务的执行优先级原则是:

完成一个宏任务后,执行余下的微任务

同一次事件循环中,宏任务永远在微任务之前执行。

ok,知道了优先级原则后,我们来看一段代码:

console.log(1);

setTimeout(function() {
    console.log(2);
    new Promise(resolve => {
        console.log(3);
        resolve(4);
        console.log(5);
    }).then(data => {
        console.log(data);
    });
}, 0);

new Promise(resolve => {
    console.log(6);
    resolve(7);
    console.log(8);
}).then(data => {
    console.log(data);
});

setTimeout(function() {
    console.log(9);
}, 0);

console.log(10);

output:  
第一次循环:
// 1
// 6
// 8
// 10
// 7

第二次循环:
// 2
// 3
// 5
// 4


第三次循环
// 9

我们一起来分析以上代码:

进入第一次事件循环,script 这个宏任务,输出 1

第一个 setTimeout 函数本身是函数调用,属于任务源,setTimeout 的回调函数,即第一个参数,才是被分发的任务,任务被加入宏任务队列,第二次循环时调用。

Promise 属于微任务,但是 Promise 初始化中代码会立即进行。所以会立即输出 6 和 8;

Promise 初始化后的回调放入微任务队列

第二个 setTimeout 也属于宏任务源,回调函数的任务放入宏任务队列,第三次事件循环时调用

继续调用栈,输出 10, 没毛病

第一次事件循环的宏任务执行完毕,执行余下的所有微任务,所以输出 7,

第二次事件循环,发现有宏任务,即第一个 setTimeout 的回调,输出 2,调用 Promise 构建函数的调用栈,直接执行,所以输出3 和 5

第一个 setTimeout 的 promise 回调放入微任务队列。

第二次事件循环的宏任务调用执行完,执行刚才前一步 Promise 创建的微任务,输出 4,第二次循环执行完毕。

进入第 3 次事件循环,只有一个宏任务,即第二个 SetTimeout,所以输出 9;

关于事件循环宏任务和微任务的执行过程:

首先两个类型的任务都是逐个执行

微任务会前下一个渲染或垃圾回收前全部执行完

一次事件循环中先只执行一个宏任务,在下一次事件循环前执行完所有的微任务,包括新创建的微任务。

五、web worker

尽管 HTML5 新标准加入了 web worker 的多线程技术,但是 web worker 只能用于计算,并且 JS 的多线程 worker 无法操作 DOM, 不然就无法控制页面是在被谁操作的了。

主线程传给子线程的数据是通过拷贝复制,同样子线程传给主线程的数据也是通过拷贝复制,而不是共享同一个内存空间。

以上说明,JS 不存在线程同步,所以还是可以把 JS 看做单线程模型,把 web worker 当做 JS 的一种回调机制。

总结

事件循环是 JS 和 Nodejs 事件调用机制的核心,保证了页面可以有序无阻塞的进行。

事件循环的主要逻辑是先执行调用栈,直到清空调用栈只剩下全局上下文。

然后 JS 检查宏任务队列,如果有任务则取出一个进行调用,进行页面渲染和垃圾回收。

同时将所有的微任务源派发的任务加入微任务事件队列,最后执行余下的所有微任务。微任务执行后完,进行页面渲染和垃圾回收后进行下一轮事件循环。

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99366.html

相关文章

  • JS专题数组去重

    摘要:将元素作为对象的键,默认键对应的值为如果对象中没有这个键,则将这个元素放入结果数组中去。 前言 数组去重在日常开发中的使用频率还是较高的,也是网上随便一抓一大把的话题,所以,我写这篇文章目的在于归纳和总结,既然很多人都在提的数组去重,自己到底了解多少呢。又或者是如果自己在开发中遇到了去重的需求,自己能想到更好的解决方案吗。 这次我们来理一理怎么做数组去重才能做得最合适,既要考虑兼容性,...

    only_do 评论0 收藏0
  • JS异步编程callback

    摘要:而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果而是在调用发出后,被调用者通过状态通知来通知调用者,或通过回调函数处理这个调用。总结回调函数是异步编程中的基石,但同时也存在很多问题,不太适合人类自然语言的线性思维习惯。 为什么 JS 是单线程? 众所周知,Javascript 语言的执行环境是单线程(single thread)。 所谓单线程,就是指一次只能完成一...

    superw 评论0 收藏0
  • JavaScript专题系列文章

    摘要:专题系列共计篇,主要研究日常开发中一些功能点的实现,比如防抖节流去重类型判断拷贝最值扁平柯里递归乱序排序等,特点是研究专题之函数组合专题系列第十六篇,讲解函数组合,并且使用柯里化和函数组合实现模式需求我们需要写一个函数,输入,返回。 JavaScript 专题之从零实现 jQuery 的 extend JavaScritp 专题系列第七篇,讲解如何从零实现一个 jQuery 的 ext...

    Maxiye 评论0 收藏0
  • JS专题垃圾回收

    摘要:如果没有引用指向该对象零引用,对象将被垃圾回收机制回收。经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的左右。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。 前言 在讲 JS 的垃圾回收(Garbage Collection)之前,我们回顾上一篇《JS专题之memoization》,memoization 的原理是以参数作为 key,函数结果作为 v...

    liujs 评论0 收藏0
  • [面试专题]JS异步原理(事件,队列)

    摘要:全部代码是一个先执行一个执行过程中遇到等异步操作则创建一个遇到等创建一个这两个分别被挂起执行栈为空时开始处理完成后处理直到该全部执行完然后继续主线程调用栈注每一次事件循环,只处理一个。 JS异步原理(事件,队列) 调用栈 JS执行时会形成调用栈,调用一个函数时,返回地址、参数、本地变量都会被推入栈中,如果当前正在运行的函数中调用另外一个函数,则该函数相关内容也会被推入栈顶.该函数执...

    LeoHsiun 评论0 收藏0

发表评论

0条评论

mengera88

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<