• 事件循环的学习、执行上文、this、执行栈和任务队列


    事件循环

    对于js运行中的任务,js有一套处理收集,排队,执行的特殊机制,我们称之为事件循环(Even Loop).
    🍕js一大特点就是单线程同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Even Loop 包含两类:Browsing Context,一种是基于 Worker.二者的运行是独立的,每个js运行的 “线程环境”都有一个独立的 Even Loop 。

    单线程

    🍔与他的用途有关。浏览器脚本语言,js的主要用途是与用户互动,以及操作DOM。这个决定了它只能是单线程,否则会带来很复杂的同步问题。
    从诞生之日起就是一门单线程的非阻塞的脚本语言。最初的用途来决定的:与浏览器交互。
    • 🍟而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
    • 🚗🚗🚗单线程是必要的,也是 JavaScript 这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的 dom 操作。试想一下 如果 JavaScript 是多线程的,那么当两个线程同时对 dom 进行一项操作,例如一个向其添加事件,而另一个删除了这个 dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,JavaScript 选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
    • 单线程在保证了执行顺序的同时也限制了 JavaScript 的效率,因此开发出了 web worker 技术。这项技术号称让 JavaScript 成为一门多线程语言。
    • 使用 web worker 技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行 I/O 操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了 JavaScript 语言的单线程本质。

    另一个特点 “非阻塞”,和事件循环的关系

    执行上下文

    总共有三种类型

    • 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
    • 函数执行上下文:存在无数个,在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
    • Eval 函数执行上下文:指的是运行在 eval 函数中的代码,很少用而且不建议使用。

    执行上下文的创建

    创建阶段
    • 1、确定 this 的值,也被称为 This Binding。
    • LexicalEnvironment(词法环境) 组件被创建。
    • VariableEnvironment(变量环境) 组件被创建

    this绑定

    -全局执行上下文中,this 的值指向全局对象,在浏览器中 this 的值指向 window 对象,而在 nodejs 中指向这个文件的 module 对象。

    • 函数执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new 绑定、箭头函数,

    词法环境

    有两个组成部分 = 自己的 + 外部引用
    • 环境记录:存储变量和函数声明的实际位置
    • 对外部环境的引用:可以访问其外部词法环境

    词法环境有两种类型

    • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
    • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

    变量环境

    • 变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性,在 ES6 中,他们的区别在于
    • 词法环境用于存储函数声明和变量( let 和 const )绑定,变量环境仅用于存储变量( var )绑定。
    • 变量提升和函数提升的原因:在创建阶段,函数和变量的声明已经存储在了执行上下文中,var声明的变量会被设置为 undefined,而在 let 和 const 的情况下,则会保持未初始化。

    三、执行栈与任务队列

    执行栈(Execution Stack)

    • 执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
    首次运行 JS 代码时,会创建一个全局执行上下文并 Push 到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并 Push 到当前执行栈的栈顶
    根据执行栈 LIFO 规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中 Pop 出,上下文控制权将移到当前执行栈的下一个执行上下文。
    • 当 Javascript 引擎遇到一段可执行代码时,新建一个执行上下文。
    • 将它推入执行栈中。并设置为运行时执行上下文。
    • 如果存在其他执行上下文。
    • 那么将当前执行上下文挂起
    • 然后再将新执行上下文推入执行栈中。
    • 执行上下文运行完毕,弹出销毁恢复并将原执行上下文设为运行时。

    任务队列(Task Queue)

    根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。
    在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
    • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
    • 检查是否存在 微任务(Microtasks),如果存在则不停地执行,直至清空
    • 更新 render
    • 主线程重复执行上述步骤
    在上述 tick 的基础上需要了解以下几点:
    • JS 分为同步任务和异步任务
    • 同步任务都在主线程上执行,形成一个执行栈
    • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
    • 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行
    同步任务
    同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务
    异步任务
    异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
    常见异步任务
    • ajax 的回调函数
    • DOM 事件的回调函数
    • setTimeout、setInterval 定时器的回调函数

    四、事件循环机制

    在这里插入图片描述

    • JS 线程在同一时间只执行一个任务,期间可能创建多个函数执行环境,对应 Frame。
    • 在执行任务的时候,随时执行环境栈的动态变化,相对应的变量对象不断创建销毁。对应 Object。
    • 异步任务 ajax I/O 等得到结果时,会将其回调作为一个任务添加到任务队列,排队等待执行
    • 当 JS 线程中的任务执行完毕,会读取任务队列 Queue,并将队列中的第一个任务添加到 JS 线程中并执行。
    • 循环 3 4 步,异步任务完成后不断地往任务队列中添加任务,线程空闲时从任务列表读取任务并执行。
    事件循环下的宏任务与微任务
    通常我们把异步任务分为宏任务与微任务,它们的区分在于:
    • 宏任务(macro-task):一般是 JS 引擎和宿主环境发生通信产生的回调任务,比如 setTimeout,setInterval 是浏览器进行计时的,其中回调函数的执行时间需要浏览器通知到 JS 引擎,网络模块, I/O 处理的通信回调也是。包含有 setTimeout,setInterval,DOM 事件回调,ajax 请求结束后的回调,整体 script 代码,setImmediate。
    • 微任务(micro-task):一般是宏任务在线程中执行时产生的回调,如 Promise.then() .catch()(本身是同步任务),process.nextTick(node),Object.observe(已废弃), MutationObserver(DOM 监听)async/await,这些都是 JS 引擎自身可以监听到回调
    优先级
    • 宏任务优先级,主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
    • 微任务优先级,process.nextTick > Promise > MutationObserver
      在这里插入图片描述
    运行机制
    在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

    在这里插入图片描述

    五、浏览器环境下的事件循环机制

    常见宏任务
    • script(整体代码)
    • setTimeout
    • setInterval
    • I/O
    • 网络请求
    • UI 交互事件(HTML解析、鼠标事件、键盘事件)
    • postMessage
    • MessageChannel
    常见微任务
    • Promise.then() .catch()
    • Object.observe
    • MutationObserver
    microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。
    所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个宏任务 (macrotask) 执行完后,就会将在它执行期间产生的所有微任务 (microtask) 都执行完毕(在渲染前)。
    setTimeout(() => {
        //执行后 回调一个宏事件
        console.log('内层宏事件3')
    }, 0)
    console.log('外层宏事件1');
     
    new Promise((resolve) => {
        console.log('外层宏事件2');
        resolve()
    }).then(() => {
        console.log('微事件1');
    }).then(()=>{
        console.log('微事件2')
    })
    外层宏事件1
    外层宏事件2
    微事件1
    微事件2
    内层宏事件3
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    这六款 Linux 常用远程连接工具,你有必要了解一下
    网络空间内生安全数学基础(1)——背景
    Python 文件写入操作
    在Spring Cloud项目中集成Springdoc OpenAPI生成OpenAPI 3文档的详细解析
    数组转换字符串
    代码随想录-026-15.三数之和
    为什么选择快速应用开发
    React@16.x(27)useCallBack
    【云原生】Rancher 从搭建到使用详解
    跑步装备品牌排行榜,2022年跑者必备好物推荐
  • 原文地址:https://blog.csdn.net/weixin_44423378/article/details/129203829