美文网首页让前端飞
javascript 并发模型与事件循环

javascript 并发模型与事件循环

作者: EdmundChen | 来源:发表于2019-02-12 17:25 被阅读0次

    JavaScript 的并发模型基于“事件循环”。

    一、运行时概念

    可视化描述

    Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack),程序中代码依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的队列(queue)中【即JavaScript执行引擎的主线程拥有一个执行栈/堆和一个任务队列

    • 栈: 函数调用形成了一个栈帧
    function foo(b) {
      var a = 10;
      return a + b + 11;
    }
    
    function bar(x) {
      var y = 3;
      return foo(x * y);
    }
    
    console.log(bar(7)); // 返回 42
    

    当调用 bar 时,创建了第一个帧 ,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量。当 foo 返回时,最上层的帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 返回的时候,栈就空了。

    • 堆: 对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。
    • 队列: 一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。
      事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
      函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

    二、事件循环(Event Loop)

    Event Loop
    • queue : 如上文的解释,值得注意的是,除了IO设备的事件(如load)会被添加到queue中,用户操作产生 的事件(如click,touchmove)同样也会被添加到queue中。队列中的这些事件会在主线程的执行栈被清空时被依次读取(队列先进先出,即先被压入队列中的事件会被先执行)。
    • callback: 被主线程挂起来的代码,等主线程执行队列中的事件时,事件对应的callback代码就会被执行

    【注:因为主线程从”任务队列”中读取事件的过程是循环不断的,因此这种运行机制又称为Event Loop(事件循环)

    console.log(1);
    setTimeout(function() {
        console.log(2);
    },5000);
    console.log(3);
    //输出结果:
    //1
    //3
    //2
    
    解释:
      1. JavaScript执行引擎主线程运行,产生heap和stack
      1. 从上往下执行同步代码,log(1)被压入执行栈,因为log是webkit内核支持的普通方法而非WebAPIs的方法,因此立即出栈被引擎执行,输出1
      1. JavaScript执行引擎继续往下,遇到setTimeout()t异步方法(如图,setTimeout属于WebAPIs),将setTimeout(callback,5000)添加到执行栈
      1. 因为setTimeout()属于WebAPIs中的方法,JavaScript执行引擎在将setTimeout()出栈执行时,注册setTimeout()延时方法交由浏览器内核其他模块(以webkit为例,是webcore模块)处理
      1. 继续运行setTimeout()下面的log(3)代码,原理同步骤2
      1. 当延时方法到达触发条件,即到达设置的延时时间时(5秒后),该延时方法就会被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立
      1. JavaScript执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行。
      1. 将队列的第一个回调函数重新压入执行栈,执行回调函数中的代码log(2),原理同步骤2,回调函数的代码执行完毕,清空执行栈
      1. JavaScript执行引擎继续轮循队列,直到队列为空
      1. 执行完毕

    三、微任务(Macrotask) 和 宏任务(Microtask)

    Event Loop 2
    不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask)宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
    Event Loop 执行顺序如下所示:
      1. 首先执行同步代码,这属于宏任务
      1. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
      1. 执行所有微任务
      1. 当执行完所有微任务后,如有必要会渲染页面
      1. 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
    实例代码
    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')
    // script start 
    // async2 end 
    // Promise 
    // script end 
    // promise1 
    // promise2 
    // async1 end 
    // setTimeout
    
    微任务包括: promiseMutationObserver
    宏任务包括: scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

    这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。

    四、永不阻塞

    事件循环模型的一个非常有趣的特性是,与许多其他语言不同,JavaScript 永不阻塞。 处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR请求返回时,它仍然可以处理其它事情,比如用户输入。

    遗留的例外是存在的,如 alert 或者同步 XHR,但应该尽量避免使用它们。

    参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
    前端面试之道
    https://blog.kaolafed.com/2017/04/21/JavaScript%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%9E%8B%E4%B8%8EEvent%20Loop/

    相关文章

      网友评论

        本文标题:javascript 并发模型与事件循环

        本文链接:https://www.haomeiwen.com/subject/qnskeqtx.html