js异步从入门到放弃(一)- Event Loop模型

前言

异步一直是前端开发里最让人头疼的一个难点,接下来的几篇文章,将围绕这个话题展开。html

1. 单线程的语言-JavaScript

众所周知,JS最初的目的是用于处理浏览器的用户交互和操做DOM,所以,若是JS设计成容许同时存在2个以上的线程,就会出现如下这种问题:前端

2个线程同时操做了同一个DOM节点(a线程要编辑该节点,而b线程删除该节点),那么此时浏览器将没法处理,由于没法判断以哪一个线程为基准。所以,JS只能是单线程。Web Worker API虽然提供了多线程,但只是纯粹基于使用多核cpu的计算能力,其建立的子线程严格受控,不影响JS单线程的设计实质
,单线程的设计就意味着,任务以排队的方式依此执行。ajax

基于单线程设计,不可避免的遇到一个情形:某些任务须要的时间很长,但不是由于任务自己太过复杂,难以处理,而是输入输出太慢(例如Ajax获取数据)。而在等待输入输出的过程当中,CPU是闲置的,为了充分利用资源,这一类任务被设计成容许暂时挂起,等到有告终果再执行的任务。segmentfault

如今有两种任务了:同步任务和异步任务api

接下来介绍JS的处理机制。promise

2. Event Loop

理论基础

首先看来自MDN的一张图:浏览器

image

  • 栈(stack),函数调用堆栈。

    看这个例子:多线程

    function a(){
            console.log('a')
        }
    
        function b(){
            console.log('from')
            a() // 这里调用了函数a
        }
        b()

    在Chrome中运行,而且单步调试,能够看到如下步骤:
    图1
    图2闭包

    1. 执行b()时,函数b进栈(如图1)
    2. b中调用函数a时,a继续进栈(如图2)
    3. 函数a执行完毕,出栈(如图1)

这部份内容实际上对应着以前介绍闭包时,函数做用域链的生成部分,传送门dom

  • 堆(heap),内存区,用于存储对象。(这个目前不是很重要先不用管)
  • 队列(queue),待处理消息队列, 每个消息都关联着一个用以处理这个消息的函数。

    常见示例:

    1. 让页面中的某个按钮,点击时触发handleClick函数,那么,当用户触发点击按钮的动做时,会有一个待处理消息进入queue,关联的函数为handleClick
    2. 发起一个ajax请求,当请求有结果以后,会有一个待处理消息进入queue,关联的函数为所指定的回调函数

总体运行过程

总体的执行过程以下(如图):
图片描述

  1. 主线程执行同步代码,执行过程会产生对应的函数调用栈stack,若是碰到有异步事件,如发起ajax请求,则提交给对应的异步模块处理,当异步任务有结果时,异步模块负责在消息队列中添加待处理的消息;
  2. 当同步任务处理完成,函数调用栈清空时,主线程检查消息队列queue:若是消息队列不为空,那么从消息队列头部取出一个待处理的消息,进入主线程;
  3. 主线程重复以上过程

上述过程循环执行,因此称为事件循环(Event Loop)

// 简单的例子
 var req = new XMLHttpRequest(); 
    req.open('GET', url);    
    req.onload = function (){}; //指定回调函数, 这是一个异步任务,会被先提交到异步处理的api,等有告终果才会添加到消息队列
    req.send();

*任务队列类型

补充说明如下,任务队列分红2类:

  1. microtask queue:ES6 的 promise产生的任务队列
  2. macrotask queue:除microtask queue之外的任务产生的任务队列,如(事件触发 setTimeout Ajax请求)

他们的区别下次讲解Promise时再说明(挖个坑)

3.定时器

上述Event Loop模型中,消息队列的新消息来源,除了有dom事件操做,ajax请求等,也多是定时任务,也就是由setTimeout建立的任务。这个函数你们确定不陌生,可是也可能未必真的足够熟悉~。

setTimeout接受两个参数:

  1. 回调函数
  2. 延迟执行的毫秒数。(严格来讲,应该是实际加入到主线程的最小延迟时间,为何呢,往下看)

如今看下如下2个例子:

//示例1
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
// 输出结果 1 3 2 ,由于setTimeout指定了里面的函数要推迟1000毫秒才会执行

这个例子说明了setTimeout的基本做用,比较简单很少说。

//示例2
const s = new Date().getSeconds(); //获取当前的秒数
setTimeout(function() {
  // 输出 "2",表示回调函数并无在 500 毫秒以后当即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {//这个循环含义就是,至少要过2s,当前主线程任务才执行完毕
  if(new Date().getSeconds() - s >= 2) { 
    console.log("Good, looped for 2 seconds");
    break;
  }
}

//实际输出
Good, looped for 2 seconds
eventloop.html:15 Ran after 2 seconds

这个例子,首先使用setTimeout指定了一个500毫秒后执行的回调函数,而后使用while循环故意让当前运行超过2秒钟,根据上文的流程图可知:

其实在第500毫秒时,这个消息已经被添加到消息队列,可是因为当前的主线程并无执行完,调用栈还没有清空,因此在500毫秒不会执行setTimeout指定的回调函数。实际上,即便把上述代码中的500改为0,结果也是同样的。

简而言之,setTimeout(fn,x毫秒)的x只是指定了fn被执行的最小等待时间,息具体能在多少时间以后执行,取决于现有调用栈函数的执行进度,以及消息队列中前面的任务执行进度。

小结

本文介绍了Event Loop模型过程以及常见的任务队列的几种任务队列消息来源,这是JS异步话题的基础篇。

参考文献
MDN-EventLoop
JavaScript 运行机制详解:再谈Event Loop


惯例:若是内容有错误的地方欢迎指出(以为看着不理解不舒服想吐槽也彻底没问题);若是有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处,若是有问题也欢迎私信交流,主页有邮箱地址

相关文章
相关标签/搜索