JS异步与性能

简介: js的event机制

一、背景

看了《你不知道的javascript》上卷以及中卷之后,总结一下js的event机制。

二、事件循环

JavaScript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是Web 浏览器。处理程序中多个块的执行,且执行每块时调用JavaScript 引擎,这种机制被称为事件循环。

先通过一段伪代码了解一下这个概念:


// eventLoop是一个用作队列的数组
// (先进,先出)
var eventLoop = [ ];
var event;
// “永远”执行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到队列中的下一个事件
        event = eventLoop.shift();
        // 现在,执行下一个事件
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

这里可以看到,有一个用while 循环实现的持续运行的循环,循环的每一轮称为一个tick。对每个tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件称之为回调函数。

一定要清楚,setTimeout(..) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,宿主环境会把你的回调函数放在事件循环中,这样,在未来 某个时刻的tick 会摘下并执行这个回调。

如果这时候事件循环中已经有20 个项目了会怎样呢?你的回调就会等待。它得排在 其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么 setTimeout(..) 定时器的精度可能不高。大体说来,只能确保你的回调函数不会在指定的 时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的 状态而定。

回调



listen("click", function handler(evt){
    setTimeout( function request(){
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} );

这里我们得到了三个函数嵌套在一起构成的链,其中每个函数代表异步序列(任务,“进程”)中的一个步骤。这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(pyramid of doom,得名于嵌套缩进产生的横向三角形状)。

  • 线性跟踪

doA( function(){
    doB();
    doC( function(){
        doD();
    } )
    doE();
} );
doF();

执行顺序是?

A、F、B、C、E、D

在线性(顺序)地追踪这段代码的过程中,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以“查看”流程。而且别忘了,这还是简化的形式,只考虑了最优情况。我们都知道,真实的异步JavaScript程序代码要混乱得多,这使得这种追踪的难度会成倍增加。

我们的顺序阻塞式的大脑计划行为无法很好地映射到面向回调的异步代码。这就是回调方式最主要的缺陷:对于它们在代码中表达异步的方式,我们的大脑需要努力才能同步得上。

  • 信任问题
// A
ajax( "..", function(..){
    // C
} );
// B

这是回调驱动设计最严重(也是最微妙)的问题。它以这样一个思路为中心:有时候ajax(..)(也就是你交付回调continuation 的第三方)不是你编写的代码,也不在你的直接控制下。多数情况下,它是某个第三方提供的工具。

  • 回调设计

(1)为了更优雅地处理错误,有些API 设计提供了分离回调(一个用于成功通知,一个用于出错通知):


function success(data) {}
function error(data) {}
ajax('...', success(data), error(data));

(2)还有一种常见的回调模式叫作“error-first 风格”(有时候也称为“Node 风格”,因为几乎所有Node.js API 都采用这种风格),其中回调的第一个参数保留用作错误对象(如果有的话)。如果成功的话,这个参数就会被清空/ 置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起/ 置真(通常就不会再传递其他结果):

function respon(err, data) {
  if(err){
  // error
 }
  else {
  // success
 }
}
ajax('...', respon);

setTimeout

教科书里面的setTimeout 定义很简单
setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。广泛应用场景:定时器,轮播图,动画效果,自动滚动等等。但是setTimeout真的有那么简单吗?

for(var i=1; i<=5; i++) {
 setTimeout(function timer() {
   console.log(i);
 }, i*1000);
}

结果:以一秒的频率连续输出五个6

解答

  • 1、作用域
    这里我引用《你不知道的javascript》中的一个比喻,可以把作用域链想象成一座高楼,第一层代表当前执行作用域,楼的顶层代表全局作用域。我们在查找变量时会先在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上找,以此类推。到达顶层后(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止。
  • 2、任务队列
    事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务(macro-task)和微任务(micro-task)。XHR回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB数据库操作等I/O以及UI rendering都属于宏任务(也有文章说UI render不属于宏任务,目前还没有定论),process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5新特性)属于微任务。注意进入到任务队列的是具体的执行任务的函数。比如上述例子setTimeout()中的timer函数。另外不同类型的任务会分别进入到他们所属类型的任务队列,比如所有setTimeout()的回调都会进入到setTimeout任务队列,所有then()回调都会进入到then队列。当前的整体代码我们可以认为是宏任务。事件循环从当前整体代码开始第一次事件循环,然后再执行队列中所有的微任务,当微任务执行完毕之后,事件循环再找到其中一个宏任务队列并执行其中的所有任务,然后再找到一个微任务队列并执行里面的所有任务,就这样一直循环下去。

参考资料

《你不知道的javascript》

目录
相关文章
|
1月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
15 1
|
1月前
|
前端开发 JavaScript 数据处理
在JavaScript中,什么是异步函数执行的例子
在JavaScript中,什么是异步函数执行的例子
10 0
|
1月前
|
前端开发 JavaScript
JavaScript的异步操作
JavaScript的异步操作
|
3月前
|
数据采集 并行计算 JavaScript
实战指南:在 Node.js 中利用多线程提升性能
在 Node.js 的世界中,多线程技术一直是一个受到广泛关注的领域。最初,Node.js 设计为单线程模式。随着技术发展,Node.js 引入了多线程支持,进而利用多核处理器的强大性能,提升了应用性能。接下来的内容将深入探讨 Node.js 如何实现多线程,以及在何种场合应该采用这种技术。
|
4月前
|
负载均衡 JavaScript 算法
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
41 1
|
5天前
|
算法
Swiper库和Glide.js库的性能有何区别
Swiper和Glide.js是两个流行的响应式轮播图库。Swiper功能强大且灵活,支持多方向滑动,拥有丰富的配置和切换效果,适合复杂需求,其高性能得益于优化的算法和惰性加载。Glide.js则轻量级、快速,专注于基础功能,适合简洁需求。两者各有侧重,选择应基于项目具体需求和性能考虑。
|
13天前
|
Web App开发 缓存 JavaScript
|
22天前
|
JavaScript 前端开发
JS 单线程还是多线程,如何显示异步操作
JS 单线程还是多线程,如何显示异步操作
22 2
|
1月前
|
JavaScript 前端开发
在JavaScript中,如何优化原型链的性能?
在JavaScript中,如何优化原型链的性能?
16 2
|
2月前
|
前端开发 JavaScript
前端JavaScript中异步的终极解决方案:async/await
在深入讨论 async/await 之前,我们需要了解一下 JavaScript 的单线程和非阻塞的特性。JavaScript 是单线程的,也就是说在任何给定的时间点,只能执行一个操作。然而,对于需要大量时间的操作(例如从服务器获取数据),如果没有适当的管理机制,这种单线程特性可能会导致应用程序的阻塞。为了解决这个问题,JavaScript 引入了回调函数和后来的 Promise,用来管理这些异步操作。