56

前言

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

1. 单线程的语言-JavaScript

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

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

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

现在有两种任务了:同步任务和异步任务

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

2. Event Loop

理论基础

首先看来自MDN的一张图:

  • 栈(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)

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

  • 堆(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


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


安歌
7k 声望5.5k 粉丝

目前就职于Ringcentral厦门,随缘答题, 佛系写文章,欢迎私信探讨.