JavaScript之Event Loop

细谈Event Loop

前段时间对JavaScript的 Event Loop (事件循环机制)有些感兴趣,就去查阅了很多关于这方面的文章,感觉受益匪浅,以下是笔者个人对 Event Loop 的见解;

首先,我们得知道 JavaScript 为什么是单线程而不是多线程?

   Brendan Eich当初在发明 JavaScript 时为什么不把 JavaScript 设置为多线程开发呢,而偏偏要选择单线程呢,多线程还可以提高效率,单线程的话,同一时间只能做一件事情,会不会有点不合理

   其实,Brendan Eich当初把 JS 设置为单线程是有原因的,和它的用途有着密切的关系, JS 的主要用途就是与用户互动以及操作 DOM .导致它生而为单线程,加入它有两个线程的话,一个线程负责在 DOM 节点上添加内容,一个赋值删除这个节点,这样会导致冲突,带来的是更为复杂的同步问题,所以js只能是单线程,也只能是单线程,不过说 JS 是单线程可以是可以,不过 也可以支持多线程,那就是 NodeJs,在 V10.5.0的 NodeJs 新增了多线程, NodeJs 中可以用 cluster 模块实现多进程,clusterfork() 可以从主进程分裂出子进程.这篇文章主要以单线程为主,暂时就不演示 NodeJs 的多线程操作了.

   所以JS为了避免复杂性,从一诞生就是单线程,所以 js 的单线程已成为这门语言的核心特征了

   为了利用充分 CPU 的计算能力, HTML5 提出了 Web Worker 标准,允许脚本创建多个线程,但是子线程完全受子线程控制,且不得操作DOM,所以,这个标准并没有改变 JS 单线程的本质,既然提到了这一标准,笔直就来演示一下代码操作吧.

  • index.js
    let woker = new Worker("worker.js");//创建对象
        
        worker.postMessage("worker.js收到请回答");//发送数据
        
        worker.onmessage = function (e){//接收并处理数据
            let res = e.data;//e.data就是worker.js文件发送给index.js的数据
            alert(res);
        }
  • worker.js
    self.onmessage = function(e){
        let res = e.data;
        self.postMessage("over!over!已收到");
    }

以上就是简单的代码演示.

任务队列

   我们继续回到单线程的问题上来,既然是单线程,这就意味着所有任务都需要在这个线程上排队,只有前一个任务结束才会执行下一个任务,如果前一个任务耗时比较长,例如网络请求,再加上还在2G时代的话,又或者是 I/O 操作的话,那岂不是要一直等到当前任务执行完毕才可以执行下一个任务了吗?假如排队的原因是因为任务的业务逻辑过于复杂/数据处理的数据较为庞大,导致 CPU 忙不过来倒是可以谅解,关键是很多时候 CPU 是空闲的,因为 IO设备很慢,不得不等待起结束后才可以执行下一个任务,设计者在设计之初也意识到这个问题,认为这种情况下完全可以不管IO设备,先把它挂起,运行后面的任务.等其返回了结果,再把挂起的任务继续执行下去.

   于是在 JS 的单线程上存在这两种任务,分别是 同步任务(Synchronous)和 异步任务 (Asynchronous)

任务类别 内容
同步任务 在主线程上排队执行的任务,只有前一个任务执行完毕才会执行下一个任务
异步任务 不加入主线程,而是进入 任务队列(task queue)的任务,只有任务队列告知主线程当前队列的某个任务可以执行了,该任务才会进入主线程执行

更为具体来说

  1. 所有同步任务都在主线程上执行,形成一个执行栈
    栈:先进后出/后进先出

  2. 主线程外还有一个任务队列,只要异步任务有了运行结果,就会在任务队列中放置一个事件

  3. 只有执行栈中的所有同步任务全部执行完毕,系统才会读取任务队列,看看里边有哪一些事件,然后对应的异步任务就会结束等待状态,进入执行栈,开始执行

  4. 主线程会不断执行第三步

事件与回调函数
  • 任务队列就是一个事件的队列,每当有异步任务有了运行结果,就会在任务队列中放置一个与之对应的事件,这个事件就表示放置这个事件的异步任务可以进入执行栈了,主线程读取任务队列就是读取里面有哪一些事件
  • 任务队列中的事件其实可不止IO设备的事件,还包括一些用户产生的事件(比如页面点击,键盘操作等事件).主要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取,然后进入执行栈执行.
    所谓的回调函数,就是那一些被主线程挂起来的代码,异步任务必须指定回调函数,但主线程执行异步任务,就是执行这些异步任务对应的回调函数.
    -任务队列是一个先进后出的数据结构,所以排在前面的事件会被主线程优先读取,主线程的读取基本是自动的,主要执行栈一清空,任务队列里的第一个事件就会进入主线程的,然后在执行栈中执行与第一个事件相对应的异步任务,但是,JS中还存在着定时器,所以主线程首先要检查下执行的时间,因为有些事件只有到了规定的时间才能返回到主线程的

Event Loop

   因为之前提到过,主线程读取任务队列中的事件这一过程是循环不断的,所以这种机制称之为 Event Loop(事件循环机制)

当主线程运行的时候,会产生堆(Heap)和栈(Stack),栈中的代码会调用各种Web API,让他们在任务队列中加入各种事件(Click,Load,Error等),只要栈中的代码执行完毕,主线程就会去读取任务队列中那些事件的回调函数,执行栈中的代码(同步任务)总是在读取任务队列(异步任务)前执行的,说这么多有点枯燥,举个栗子

    //在这里就不考虑XHRHttpRequest的兼容性了
    let xhr = new XHRHttpRequest();
        xhr.open("get",url)
        xhr.onLoad = function(){}
        xhr.send()

因为上述代码的send()方法是Ajax往服务器发送数据,所以它属于异步任务,所以也可以有下面这种写法

 let xhr = new XHRHttpRequest();
        xhr.open("get",url)
        xhr.send()
        xhr.onLoad = function(){}

也就是说,指定函数的部分onload,在send()方法的前后都无所谓,因为它们属于执行栈的一部分,系统总是执行完它们才会去读取任务队列

ps:以上很绕,要好好理解,笔者我也是搞了很久才有点懂Emmm

定时器

   除了放置异步任务的事件,任务队列还可以放置定时事件,即指定某些代码在什么时候执行,这叫做"定时器"(timer)功能,就是定时执行的代码

   定时器主要分为两种,一种是setTimeout()setInterval(),它们的内部的运行机制是完全一样的,唯一不同的就是前者内部的回调函数是一次性执行的,而后者是周期新反复执行的

setTimeout()普遍接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数,第三个以及第三个后的参数为回调函数的参数

console.log(1);
setTimeout(() => console.log(2),1000);
console.log(3)

[ 结果是 1,3,2 ] 可能有人认为它是因为推迟 1s 才执行的原因导致的

那么我们把推迟的时间调成 0ms 试试看

console.log(1);
setTimeout(() => console.log(2),0);
console.log(3)

[ 结果是 1,3,2 ] 这是为什么呢?

因为setTimeout内部的回调函数属于异步任务,所以执行栈会先把同步任务先执行完毕后,才会去执行任务队列的回调函数

   HTML5标准规定setTimerout的第二个参数即延迟时间的最小值不得低于 4ms ,如果设置的延迟时间低于4ms,就会自动增加. 在此之前,老版本的浏览器都将最短时间设置为 10ms,此外,对于那些DOM操作导致页面重新渲染的部分,通常不会立即执行的,而是每 16ms执行一次,所以同样在操作DOM制作动画的情况下,使用requestAnimationFrame()(请求动画帧)的效果好于用定时器. 另外还有一点就是定时器只是将回调函数插入到任务队列,所以必须要等到当前代码即 执行栈执行完毕,主线程才会去执行他的回调函数,要是当前代码执行耗时过于长,有可能会等待很久才可以执行,所以不能保证定时器内部的回调函数一定会在指定的时间执行

你可能感兴趣的:(JavaScript之Event Loop)