最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浏览器专题系列 - 事件循环机制

    正文概述 掘金(粥里有勺糖)   2021-02-09   626

    浏览器专题系列 - 事件循环机制

    前言

    先简单聊聊JavaScript与此主题有关的其它内容,加深读者对内容的吸收与理解

    解释性语言

    脚本语言编写的代码通常是逐行解释执行而非编译(c/c++,java),所以通常又叫做解释性语言

    所以javascriptpython,shell一样,也是一门优秀的解释性语言

    制约解释性语言的性能瓶颈之一就是解释器,好在javascript有著名的V8(Android,Chrome)与JSCore(IOS,Safari)等优秀的解释器引擎,js得以大范围推广,它们是必不可少的功臣

    单线程模型

    javascript语言的一大特点就是单线程,即同一时间只能做一件事

    为什么是单线程?

    作为浏览器脚本语言,javascript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题

    例如:假定javascript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器不知道应该以哪个线程为准?

    所以,为了避免复杂性,从一诞生,javascript就是单线程的

    单线程的优势

    • 不会出现因线程之间争夺资源导致的死锁现象
    • 所有代码都是同步执行的
    • 没有线程切换的资源开销

    单线程的缺点

    • 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着

    任务队列

    浏览器中存在有很多耗时的任务的场景,网路请求(ajax),监听事件的传递,定时器等等

    javascript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去

    于是对所有任务进行了划分,分为 同步任务异步任务

    同步任务

    在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

    即顺序执行

    异步任务

    不进入主线程、而进入任务队列的任务

    只有当任务队列通知主线程,某个异步任务可以执行了的时候,该任务才会进入主线程执行

    当解释器引擎遇到异步任务时,会将其挂起,在时机成熟时,将其回调函数放入到任务队列中

    打个比方,就像炒菜一样,锅里的东西一直在翻炒着(主线程),各种调料(不同的异步任务)只会在需要的时候被加入锅里,然后完成其使命

    异步机制

    1. 所有同步任务都在主线程上执行,形成一个执行栈
    2. 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
    3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行

    只要主线程空了,就会去读取"任务队列",这就是javascript的运行机制。主线程不断重复上面的第3步

    事件与回调

    "任务队列"是一个事件的队列,IO设备(鼠标,键盘等)完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件

    只要指定过回调函数,这些事件(鼠标点击,键盘按键,页面滚动等)发生时就会进入"任务队列",等待主线程读取

    所谓"回调函数",就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数

    队列有先进先出的特性,主线程会优先读取任务队列最前面的事件

    主线程的读取过程基本上是自动的,只要”执行栈“一清空,"任务队列"上第1位的事件就会自动进入主线程

    针对于定时器事件,主线程会先检查一下执行时间,只有到了规定的时间,才能返回给主线程,即到了一定时间后才把事件对应的回调函数放入执行栈中

    Event loop

    什么是Event Loop

    也就是通常说的事件循环

    js的事件循环负责执行代码、收集和处理事件以及执行队列中的子任务,与其它语言的模型截然不同

    js的事件循环模型与许多其他语言相比有一个非常有趣的特性是,它永不阻塞,处理 I/O 通常通过事件和回调来执行

    因此当一个应用正等待一个 AJAX 请求返回时,它仍然可以处理其它事情,如用户输入,鼠标点击/滚动等

    什么是执行栈

    执行栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则

    js开始执行代码的时候会首先创建一个main函数,然后根据执行的代码,根据先进后出的原则,后执行的函数先弹出栈

    这里有一个可视化执行栈的在线工具 -> Loupe

    示例

    function a(v){
        return v*4
    }
    function b(v){
        return a(v*3)
    }
    console.log(b(2))
    

    进栈顺序

    1. main()
    2. console.log(b(2))
    3. b(2)
    4. a(6)
    

    出栈顺序

    1. a(6)  // 24
    2. b(2)  // 24
    3. console.log(b(2)) // 24
    4. main()
    

    使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈(如下图所示)

    浏览器专题系列 - 事件循环机制

    浏览器中的 Event Loop

    通过上面的阐述,大概也是知道了js是如何执行的,了解了如何以单线程同步执行的方式处理异步任务的机制,下面开始详细描述一下执行的过程

    执行 js 代码的时候其实就是往执行栈中放入函数/回调函数,当遇到异步的代码时,会被挂起并在需要执行的时候加入到 任务队列(有多种)中,一旦执行栈为空,时间循环机制 就会从 任务队列中拿出需要执行的代码并放入执行栈中执行

    所以本质上来说 js 中的异步依旧还是同步的行为

    任务源

    不同任务源会被分到不同的任务队列中

    微任务

    • promise
    • MutationObserver
    • ...

    宏任务

    • script
    • xhr
    • setTimeout
    • setInterval
    • requestAnimationFrame
    • I/O
    • UI rendering
    • ...

    Event Loop执行顺序

    这里先抛出浏览器中js的事件循环的不同任务的执行顺序

    在事件循环中,每进行一次循环操作称为 tick

    1. 执行所有的同步代码
    2. 执行完所有同步代码后,执行栈为空,查询是否有异步任务需要执行
    3. 执行 微任务,如果在执行 微任务 的过程中,又产生了 微任务,那么会加入到 微任务的队列 的末尾,也会在这个周期被调用执行
    4. 执行完所有微任务后,如有必要会渲染页面:
      • 判断document是否需要更新
        • 大部分显示设备还是 60Hz 的刷新率,所以 16.6ms 才会更新一次渲染
      • 判断是否有 resize 或者 scroll 事件,有的话会去触发事件
        • 所以 resize 和 scroll 事件也是至少 16.6ms 才会触发一次,即自带节流功能。
      • 判断是否触发了 media query(媒体查询)
      • 更新动画并且发送事件
      • 判断是否有全屏操作事件
      • 执行 requestAnimationFrame 回调
      • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上
      • 更新界面
    5. 开始下一轮 Event Loop ,从宏任务中取出一个执行,再然后微任务...

    小结归纳

    • 宏任务一次只从宏任务队列中取一个任务执行,执行完后就去执行微任务队列中的任务
    • 微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空;
    • 执行UI rendering,它的时间节点是在执行完所有的微任务之后,下一个宏任务之前
    • 定时器不是绝对准确的
      • SetTimeout/SetInterval只是在指定时间后将其回调函数放入到宏任务队列中

    浏览器专题系列 - 事件循环机制

    示例

    下面通过一个示例来阐述一下代码的执行顺序

    console.log('script start')
    
    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')
    
    1. 执行同步代码 输出 script start
      • console.log('script start')
    2. 执行async1中的同步代码输出async2 end
      • async1()
      • async2()
        • 因为函数有async标志,所以返回的是Promise,我们记为P1
      • console.log('async2 end')
      • await
        • await为让出线程的标志,即执行await async2()后就回到 async1外部
        • 而await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator,所以这里又套上了一层Promise,我们记为P2,其包裹着P1
    3. 遇到异步任务定时器,我们先给它挂起,记为 S1
    4. 执行new Promise 的构造函数,输出 Promise
      1. console.log('Promise')
      2. resolve()
      • 生成新的微任务记为 P3
    5. 执行同步代码输出 script end
    6. 此时所有的同步代码执行完成,微任务与宏任务队列情况分别是
      1. 微任务:[P2,P3]
      2. 宏任务:[S1]
    7. 执行所有的微任务
      1. 取出P2执行,产生新的微任务 P1加入到队列中 [P3,P1]
      2. 取出P3执行, 打印输出promise1,产生新的微任务 P4 加入队列中 [P1,P4]
      3. 取出P1执行, 无输出,产生新的微任务 P5 加入队列中 [P4,P5]
      4. 取出P4执行, 输出promise2,无新的任务产生 [P5]
      5. 取出P5执行, 输出async1 end,无新的任务产生 []

    低版本浏览器的结果为

    // script start --> async2 end --> Promise --> script end --> promise1 --> promise2
    // async1 end --> setTimeout
    

    因为 await 后面跟着 Promise 的话,async1 end 需要等待3个 microtick 才能执行到

    async1 其等价的v8优化前的旧版代码为

    function async1(){
      new Promise((resolve)=>{
        const p = new Promise(res=>res(async2()))
        p.then(()=>{
          console.log('async1 end')
          resolve()
        })
      })
    }
    

    新版浏览器的结果为

    // script start --> async2 end --> Promise --> script end --> async1 end 
    //  promise1 --> promise2 --> setTimeout
    

    在本例中上述的 P2包裹P1 合并成了一个,即await后面如果是Promise将不会再进行一次Promise包装

    async1 其等价的v8优化后的代码为

    function async1(){
      new Promise((resolve)=>{
        const p = Promise.resolve(async2())
        p.then(()=>{
          console.log('async1 end')
          resolve()
        })
      })
    }
    

    小结

    1. 在新版浏览器中,await promiseFun,3个 microtick 被优化为了 2个 microtick
      • new Promise 替换为了 Promise.resolve
      • Promise.resolve的参数如果是Promise则直接返回这个Promise

    补充

    问题追溯可查看

    • 更快的异步函数和 Promise
    • v8是怎么实现更快的 await ?深入理解 await 的运行机制

    自测

    自测试1

    console.log(1);
    
    setTimeout(() => {
      console.log(2);
      Promise.resolve().then(() => {
        console.log(3)
      });
    });
    
    new Promise((resolve, reject) => {
      console.log(4)
      resolve(5)
    }).then((data) => {
      console.log(data);
    })
    
    setTimeout(() => {
      console.log(6);
    })
    
    console.log(7)
    
    点击查看答案

    输出结果

      
      // 1 4 7 5 2 3 6
      

    自测2

    console.log(1);
    
    setTimeout(() => {
      console.log(2);
      Promise.resolve().then(() => {
        console.log(3)
      });
    });
    
    new Promise((resolve, reject) => {
      console.log(4)
      resolve(5)
    }).then((data) => {
      console.log(data);
      
      Promise.resolve().then(() => {
        console.log(6)
      }).then(() => {
        console.log(7)
        
        setTimeout(() => {
          console.log(8)
        }, 0);
      });
    })
    
    setTimeout(() => {
      console.log(9);
    })
    
    console.log(10);
    
    点击查看答案

    输出结果

      
      // 1 4 10 5 6 7 2 3 9 8
      

    参考

    • 阮一峰:JavaScript 运行机制详解:再谈Event Loop
    • MDN:并发模型与事件循环

    起源地下载网 » 浏览器专题系列 - 事件循环机制

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元