EventLoop事件循环和一些浏览器小知识

js执行机制和事件循环event-loop

一些基本概念

  1. 浏览器, js, 执行引擎的关系

    js: 一门计算机语言, 提供了表达程序逻辑的语法和实现基本功能的api

    浏览器: js的真实运行环境, 又称之为js的宿主环境

    js执行引擎: js宿主环境(例如浏览器)中的一个功能模块, 用于执行和解析js

  2. 进程和线程

    进程: 当一个应用程序运行时, 需要使用内存和CPU资源, 这些资源要像操作系统申请, 操作系统以进程的方式来分配这些资源, 一个进程就代表着一块独立于其他进程的内存空间, 一个应用程序要运行, 必须至少有一个进程启动, 进程的最大特点就是独立, 一个进程不能随意的访问其他进程的资源, 这就保证了多个程序在操作系统上运行却互不干扰

    如我们手机中同时运行的微信, 王者荣耀和网易云音乐, 他们就是彼此独立的进程

    线程:一个应用程序要执行多个任务(比如我们再玩王者荣耀的时候可以一边打游戏然后王者荣耀会自动帮我们录制MVP王者时刻的视频[注意, 非我们自己录屏, 而是王者时刻], 这个时候王者荣耀就要执行多个任务), 这些任务每一个都必须在一个线程上运行, 线程和线程相对独立, 但是可以轻易的共享应用程序的数据

    浏览器中的线程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iwdYe9ay-1581524325293)('..')]

  3. 如何理解js是单线程?

    我们之所以称js为单线程的语言, 是因为他的执行引擎只有一个线程, 并且不会在执行期间开启新的线程, 而并非说浏览器是单线程(在html5中, js已经支持开启新线程 -> service worker, 但是目前确实应用特别少)

    单线程应用具有以下特点

    • 易于学习和理解: 所有的代码都是按照顺序从上到下执行的

    • 程序容易被掌控: 由于代码是按照顺序进行且单线程, 所以程序不会出现中断, 也不会出现 资源抢夺的问题, 极大的降低了开发的难度

    • 更加合理的利用计算机的资源: 创建新的线程和销毁线程都会耗费额外的cpu和内存资源, 如果没有良好的线程设计, 多线程可能比单线程效率更加低下

    举个栗子

    如果把单线程和多线程都比为餐厅里的服务员, 那么多线程就是这个餐厅有多个服务员, 那么看似效率很快, 但是也可能会造成服务员占用共同的资源的尴尬, 比如厨师只有一个, 那么一道菜出来服务员A和服务员B可能会进行争夺, 而单线程则是这个餐厅只有一个服务员, 他完全不可能出现上面的这种情况, 他自己一个人可以 有条不紊的进行操作

  4. js与多任务

    任何一个应用程序在执行期间都可能会开启多任务, 比如:

    • js自己的一些计算操作

    • 开启计时器操作

    • 监听事件操作

    由于js的执行线程只有一个, 因此他会把这些多余的任务交付给浏览器的其他线程帮忙处理

  5. 同步代码和异步代码

    同步代码: 程序启动以后, 在js线程上立马执行的代码叫做同步代码

    异步代码: 收到宿主环境(比如浏览器)的其他线程通知, 并且即将在js线程上执行的代码, 比如计时器回调函数中代码, 用户事件中的代码, js中的异步代码往往会写在一个函数中, 因此这样的函数我们称之为异步函数

  6. 执行栈

    为了保证代码的正常运行, js执行引擎使用执行栈来组织js代码

    每当调用一个函数时, 都会在执行栈中创建一个上下文, 上下文中提供了函数执行需要的环境,创建了上下文以后, 再执行函数

我们来到大头 event loop 事件循环(浏览器)

事件循环是js处理异步函数的具体方法

  1. 执行执行栈中的方法

  2. 遇到一些特殊代码交付给浏览器的其他线程进行处理

  3. 将执行栈中的方法全部执行完毕

  4. 从事件队列中取出第一个任务放入执行栈, 然后重复第一步(事件队列分微任务和宏任务, 在最后面会介绍)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Zjk9XY8-1581524325295)('..')];

我们来看一些代码

setTimeout(() => {
    console.log('hello, 我是计时器');
}, 0)

// for循环走完10000次大概是462ms左右, 但是由于是同步代码会在执行栈中一直执行完毕
// 当代码执行完毕以后
for(let i = 0; i < 10000; i++) {
    console.log(i);
}

for循环输出10000大概要462ms左右, setTimeout 设置 0ms在Chrome浏览器中会默认走4ms的延迟, 但是尽管如此, 输出结果一定是’hello,我是计时器在循环全部执行完毕以后再输出’

原因也很简单, 程序执行到setTimeout的时候发现是异步代码并且要调用浏览器的计时器线程, 于是推入webapi的计时线程开始计时, 同时将setTimeout函数移除出执行栈开始执行for循环, 由于setTimeout设定的是0s(实际上Chrome计时器设置0s也会默认走4ms的延迟)所以会马上计时完毕并且推入事件队列中等待执行, 而事件队列中的任务是必须要执行栈中的代码全部执行完毕, 再来瞅瞅队列中有无任务需要执行, 结果发现了有一个任务所以会拉入执行栈进行执行所以当’hello, 我是计时器’输出在控制台的时候for循环的一万次已经跑完

我们来看一个恶心的代码巩固一下

setTimeout(() => {
    console.log(1);
    foo();
}, 0)

function foo()  {
    setTimeout(() => {
        console.log(2);
    }, 0)
    console.log(3);
}

console.log(4);

foo();

上面这份代码的输出结果: 4, 3, 1, 3, 2, 2

第一步: js执行栈中自动生成全局上下文

第二步: 遇到setTimeout 函数 并将第一个setTimeout函数放入执行栈, 但发现这个函数是要开启计时器线程的于是将其推入webapi中并调用计时器线程由于是0秒后就执行于是一瞬间就被推入进事件队列等待调用

这个时候执行队列中第一位等着的是第一个setTimeout的执行函数

第三步: 一直运行同步代码知道遇到console.log(4), 于是在执行栈放入该函数上下文, 并且输出4, 输出完毕以后该函数说 呀呵 爷没有其他操作了, 于是销毁自己的上下文,

此时页面输出4

第四步: 运行 同步代码中的foo执行, 导致执行栈中开始执行foo的函数体, 发现有一个setTimeout同理经过一系列操作以后放入执行队列中的第二位, 同时输出3, 这哥们说爷也没事做了, 销毁吧那

此时页面再输出3

第五步: 这个时候页面中所有的同步代码已经完毕, 于是js开始看执行队列中有无需要执行的东西, 一看还真有就直接把第一位拿出来执行, 第一位就是第一次遇到的setTimeout,这哥们先执行1再说, 执行完了以后又执行foo, foo执行又将setTimeout放入执行队列的最后一位排队, 并且输出3

此时页面再输出1, 3

第六部: 将两个执行队列中的执行方法依次执行 输出2, 2

此时页面最后输出2, 2

希望这个流程我描述清楚了, 实际上还有一些更加细微的操作并不完全是这样, 具体的同学可以看看执行期上下文的js机制, 这里不多做赘述

我们最后来说说这个事件队列

事件队列在不同的宿主环境中有所差异, 大部分宿主环境会将事件队列进行细分, 在浏览器环境中,事件队列分为两种

  • 宏任务(某些称之为宏队列): macroTask, 计时器结束时候的回调, 事件回调, http回调等绝大部分异步函数进入宏任务

  • 微任务(某些称之为微队列): microTask, Promise.then, MutationObserver进入微任务

当执行栈清空时, 浏览器会优先寻找微任务并且依次执行结束, 当微任务中确实没有任务可以执行了才会去管宏任务的执行

发布了33 篇原创文章 · 获赞 11 · 访问量 2242

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/104289422