zoukankan      html  css  js  c++  java
  • js事件循环机制

     JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。为了协调这些进行函数调用或者任务的调度,任务队列就产生了。

    一、先搞懂两个东西:堆和栈

    栈由操作系统自动分配释放,用于存放函数的参数值、局部变量等一些基本的数据类型,其操作方式类似于数据结构中的栈

    堆用于存放对象(引用数据类型),一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。

    栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

    堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

    栈: 在函数调用时,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向函数的返回地址,也就是主函数中的下一条指令的地址,程序由该点继续运行。

    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

     1、栈

     函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量。

     2、堆

    var car1 = {
      name: 'huruqing',
      money: 100000000
    }
    var car2 = car1;
    car2.money = 1000;
    console.log(car1.money === car2.money); //true 1000

    var obj1 = {
      a: 2
    }
    var obj2 = {
      a: 2
    }
    console.log(obj1 === obj2); // false
    
    var arr1 = [];
    var arr2 = [];
    console.log(arr1 === arr2); // false

     栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

    二、事件循环模型

     解释:

    1. 所有代码分类
      * 初始化执行代码(同步代码): 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
      * 回调执行代码(异步代码): 处理回调逻辑
    2. js引擎执行代码的基本流程:
      * 初始化代码===>回调代码
    3. 模型的2个重要组成部分:
      * 事件管理模块(定时器/DOM事件/Ajax)
      * 回调队列
    4. 模型的运转流程
      * 执行初始化代码, 遇到同步代码立即执行,遇到异步任务将事件回调函数交给对应模块管理(定时器/DOM事件/Ajax)
      * 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
      * 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行

     

      当一个任务被执行时,js会判断是否为同步任务,同步任务和异步任务会进入不同的执行环境,所有的同步任务都会进入到主执行栈立即执行,所有的异步任务都会会被加入到对应的事件管理模块,当事件发生时管理模块会将回调函数及其数据添加到回调队列中,只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行。这一过程的不断重复就是事件循环。

     (简单点理解就是当一个任务被执行时,js会判断是否为同步任务,同步任务和异步任务会进入不同的执行环境,所有的同步任务都会进入到主执行栈,所有的异步任务都会进入到任务队列,直到主执行栈的任务执行完毕才会执行任务队列的异步任务,这一过程的不断重复就是事件循环。)

    举例子:

    function fn1() {
        console.log('fn1()')
      }
      fn1()
      document.getElementById('btn').onclick = function () {
        console.log('点击了btn')
      }
      setTimeout(function () {
        console.log('定时器执行了')
      }, 2000)
      function fn2() {
        console.log('fn2()')
      }
      fn2()

     再来看这个的输出顺序

    console.log('script start');
    
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);
    
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });
    
    console.log('script end');

     这里promise.then和setTimeout都是异步的,那为什么先输出promise.then里面的呢?

    因为promise函数是同步任务会立即执行,其后的.then是异步里面的微任务setTimeout是异步里面的宏任务先执行完微任务再执行宏任务。

    async function async1() {
    await async2()
    console.log('async1 end')
    }
    async function async2() {
    console.log('async2 end')
    }
    async1()
    
    setTimeout(function() {
    console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
    console.log('Promise')
    resolve()
    })
    .then(function() {
    console.log('promise1')
    })
    .then(function() {
    console.log('promise2')
    })
    
    console.log('script end')

    console.log('script start')
    
    async function async1() {
    await async2()
    console.log('async1 end')
    }
    async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
      console.log('async2 end1')
    })
    }
    async1()
    
    setTimeout(function() {
    console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
    console.log('Promise')
    resolve()
    })
    .then(function() {
    console.log('promise1')
    })
    .then(function() {
    console.log('promise2')
    })
    
    console.log('script end')

     async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,async2就是立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。但是有了await就不一样了,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈(后面会详述)的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列(Promise的Job Queue)

     三、补充微任务与宏任务

    宏任务:整个js代码块、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

    微任务:Promise、MutaionObserver、process.nextTick(Node.js 环境)

    其执行顺序如下图所示:

    所以,上面那个程序:

    主程序(整个js代码块)和和settimeout都是宏任务,两个promise是微任务

    第一个宏任务(主程序)执行完,执行全部的微任务(两个promise),再执行下一个宏任务(settimeout),所以输出结果就是那样的。

    四、定时器引发的思考

    定期器分为一次性定时器setTimeout与周期性定时器setInterval,前者是等待N秒之后执行回调一次没了,后者是每隔N秒执行回调一次。

    并不代表执行时间,而是将回调函数加入任务队列的时间。

    setTimeout(() => console.log('1'), 3000);
    setTimeout(() => console.log('2'), 3000);

    这两个定时器的输出顺序是什么?

    你肯定会说先输出1,在输出2,我上面说了,时间并不代表执行时间,而是告诉事件管理模块三秒后将第一个加入到任务队列,再过三秒,再将第二个加入到任务队列,等到主执行栈任务为空了在调用任务队列,至于主执行栈什么时候执行完毕那就要看里面代码的执行情况,所以定时器的执行时间不一定是3000。

     通过这个例子可以看出。

    不积跬步无以至千里
  • 相关阅读:
    JS(原生语法)_实现酷酷的动态简历
    Linux外在设备的使用
    查看系统内存信息
    查看CPU信息
    查看系统PCI设备
    配置网络
    Linux分区
    Observer
    Singleton
    Open closed principle
  • 原文地址:https://www.cnblogs.com/lyt0207/p/11956682.html
Copyright © 2011-2022 走看看