JS专题之事件循环

准备知识

1. 进程(process)

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

2. 线程(Thread)

线程是CPU调度和分派的基本单位,一个线程只能属于一个进程,一个进程能够有多个线程且至少有一个。比方说一个车间的工人,能够有多个工人一块儿工做。java

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

常常看到有不少任务同时在进行,一边工做,一边听歌,还一边下载电影。那是由于这些线程在以闪电般的速度不断的切换主要的几个线程,因此,人的体验上感受是不少不少任务在同时进行。promise

3. 栈(stack)

栈是一种数据结构,具备后进先出的特色,最开始进入栈结构的数据反而最后才能出来。 浏览器

4. 队列(queue)

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

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 之执行上下文》

2、为何 JS 是单线程模型?

JavaScript 的一个很是有趣的特性是事件循环模型,与许多其余语言不一样,它永不阻塞。 处理 I/O 一般经过事件和回调来执行 -- MDN

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

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

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

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

3、事件循环

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

事件循环的顺序是:

  1. 进入 script 标签,建立全局上下文
  2. 执行全局上下文中的函数,将其压入执行调用栈
  3. 某个函数执行完后,函数弹出执行栈,清空函数上下文中的变量对象和内存空间,判断是否须要更新渲染,若是须要则更新渲染。
  4. 若是遇到异步事件,也会压入执行调用栈,但浏览器识别到它是异步事件后,会将其弹出执行栈,而后将异步事件的回调函数放入事件队列中。
  5. 执行直到函数调用栈清空只剩全局执行上下文,这时,JS 会检查事件队列中是否有事件,若是有,则将事件队列中的一个事件出队,而后压入执行栈中执行。
  6. 当执行栈又清空只剩全局执行上下文时,又会重复第 5 步。这就是 JS 的事件循环。
  7. 当用户关闭浏览器,全局执行上下文弹出执行栈,清空相应上下文中的变量对象和内存空间。

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

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 属于异步事件,老是会在主线程的任务执行完后才开始执行。

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

  1. 一次只处理一个任务
  2. 一个任务从开始到完成,不会被其余任务所中断

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

4、宏任务和微任务

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

宏任务:
宏任务包括:

  1. 建立文档对象、解析 HTML、执行主线程代码(script)
  2. 执行各类事件:页面加载、输入、点击
  3. setTimout,setInterval 异步事件

宏任务表明一个个离散、独立的工做单元,运行完任务后,浏览器能够进行其余的任务调度,如更新渲染或执行垃圾回收。宏任务须要屡次事件循环才能执行完。

微任务:
微任务包括:

  1. Promise 回调函数
  2. 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
复制代码

咱们一块儿来分析以上代码:

  1. 进入第一次事件循环,script 这个宏任务,输出 1
  2. 第一个 setTimeout 函数自己是函数调用,属于任务源,setTimeout 的回调函数,即第一个参数,才是被分发的任务,任务被加入宏任务队列,第二次循环时调用。
  3. Promise 属于微任务,可是 Promise 初始化中代码会当即进行。因此会当即输出 6 和 8;
  4. Promise 初始化后的回调放入微任务队列
  5. 第二个 setTimeout 也属于宏任务源,回调函数的任务放入宏任务队列,第三次事件循环时调用
  6. 继续调用栈,输出 10, 没毛病
  7. 第一次事件循环的宏任务执行完毕,执行余下的全部微任务,因此输出 7,
  8. 第二次事件循环,发现有宏任务,即第一个 setTimeout 的回调,输出 2,调用 Promise 构建函数的调用栈,直接执行,因此输出3 和 5
  9. 第一个 setTimeout 的 promise 回调放入微任务队列。
  10. 第二次事件循环的宏任务调用执行完,执行刚才前一步 Promise 建立的微任务,输出 4,第二次循环执行完毕。
  11. 进入第 3 次事件循环,只有一个宏任务,即第二个 SetTimeout,因此输出 9;

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

  1. 首先两个类型的任务都是逐个执行
  2. 微任务会前下一个渲染或垃圾回收前所有执行完
  3. 一次事件循环中先只执行一个宏任务,在下一次事件循环前执行完全部的微任务,包括新建立的微任务。

5、web worker

尽管 HTML5 新标准加入了 web worker 的多线程技术,可是 web worker 只能用于计算,而且 JS 的多线程 worker 没法操做 DOM, 否则就没法控制页面是在被谁操做的了。

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

以上说明,JS 不存在线程同步,因此仍是能够把 JS 看作单线程模型,把 web worker 当作 JS 的一种回调机制。

总结

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

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

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

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

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

掘金专栏 JavaScript 系列文章

  1. JavaScript之变量及做用域
  2. JavaScript之声明提高
  3. JavaScript之执行上下文
  4. JavaScript之变量对象
  5. JavaScript之原型与原型链
  6. JavaScript之做用域链
  7. JavaScript之闭包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值传递
  11. JavaScript之例题中完全理解this
  12. JavaScript专题之模拟实现call和apply
  13. JavaScript专题之模拟实现bind
  14. JavaScript专题之模拟实现new
  15. JS专题之事件模型
相关文章
相关标签/搜索