JavaScript基础教程 如何让Bluebird更快速
沉沙 2018-06-19 来源 : 阅读 1539 评论 0

摘要:Bluebird 是一个被广泛使用的 JavaScript promise 库,最初被注意到是在 2013 年,因其实施速度比当时类似功能的其它 promise 库快了 100 倍。Bluebird 如此之快的原因在于,它对 JavaScript 优化的基础原理的运用贯穿了整个库。本文将详细介绍三种用于优化 Bluebird 的最有价值的基础知识。

Bluebird 是一个被广泛使用的 JS promise 库,最初被注意到是在 2013 年,因其实施速度比当时类似功能的其它 promise 库快了 100 倍。Bluebird 如此之快的原因在于,它对 JavaScript 优化的基础原理的运用贯穿了整个库。本文将详细介绍三种用于优化 Bluebird 的最有价值的基础知识。

1. 函数对象分配最小化

对象分配,尤其是函数对象分配,在实现时由于产生大量的内部数据,对性能造成沉重的负担。JavaScript 的实际实现是一种垃圾回收机制,所以分配的对象并不是简单地存储在内存中,垃圾回收器在不断地寻找未使用的对象,从而释放它们。在 JavaScript 中使用的内存越多,垃圾回收器占用的 CPU 资源也就越多,从而执行实际代码的 CPU 也就越少。

在 JavaScript 中,函数是第一类对象。这意味着它们和任何其它对象一样,具有相同的特征和属性。如果你有一个包含了另一个或多个函数的代码声明的函数,那么对父函数的每一次调用都会创建新的、唯一的函数对象,尽管执行了相同的代码。如下是一个基本的例子:


function trim(string) {
    function trimStart(string) {
        return string.replace(/^s+/g, "");
    }
    function trimEnd(string) {
        return string.replace(/s+$/g, "");
    }
    return trimEnd(trimStart(string))
}

   

现在每次调用 trim 函数,都会创建两个不必要的函数对象来表示 trimStrat 和 trimEnd。函数对象是不必要的,因为它们并不用于对象的唯一标识,例如属性赋值或变量封装。他们只用于代码所包含的功能。

这个例子很容易优化,只需简单地把这些函数移出 trim 。由于示例包含在模块当中,并且模块仅为程序加载一次,所以函数将只存在一种表现形式:


function trimStart(string) {
    return string.replace(/^s+/g, "");
}
 
function trimEnd(string) {
    return string.replace(/s+$/g, "");
}
 
function trim(string) {
    return trimEnd(trimStart(string))
}

   

然而,更多常见的函数对象似乎是一个无法避免的弊病,没办法这么轻易地优化。例如,任何时候你传递一个稍后会被调用的回调函数,几乎总是需要一个特定的上下文来进行回调。通常情况下,这种上下文以简单而直观但低效的闭包方式来实现。一个简单的例子就是使用默认的异步回调接口在节点中读取一个 JSON 文件:


var fs = require('fs');
 
function readFileAsJson(fileName, callback) {
 
    fs.readFile(fileName, 'utf8', function(error, result) {
 
        // This is a new function object created every time readFileAsJson is called
        //每当 readFileAsJson 被调用,都会创建新的函数对象
        // Since it's a closure, an internal Context object is also
        // allocated for the closure state
        //由于这是一个闭包,一个内部的上下文对象也会分配给这个闭包状态
 
        if (error) {
            return callback(error);
        }
 
        // The try-catch block is needed to handle a possible syntax error from invalid JSON
        //try-catch块用于处理由于无效的JSON文件导致的可能出现的语法错误
 
        try {
            var json = JSON.parse(result);
            callback(null, json);
        } catch (e) {
            callback(e);
        }
 
    })
}

   

在这个例子中,传递给 fs.readFile 的回调函数不能被移出 readFileAsJson,因为它通过唯一的变量回调创建了一个闭包。还应该注意的是,fs.readFile 无论是作为命名函数声明还是内联匿名函数来都没什么区别。

很大程度上 Bluebird 内部使用的优化是使用显示的简单对象来保存上下文数据。由一个通过多层的回调组成的操作,只需要一次这样的对象分配。每次将回调传递到另一个层时,每个层将不会创建一个新的闭包,而是将显式的简单对象作为一个额外的参数进行传递。举个例子,假设一个操作中有五个回调的步骤,使用闭包意味着分配五个函数对象和上下文对象,但是如果使用这个plain object方法来优化的话,总共只分配一个 plain object 就可以了。

我们可以修改一下 fs.readFile API,让它接受一个上下文对象,在刚刚的例子中使用这种优化方法以后,代码看起来就是这样的:


var fs = require('fs-modified');
 
function internalReadFileCallback(error, result) {
 
    // The modified readFile calls the callback with the context object set to `this`,
    // which is just the original client's callback function
    //重新修改后的 readFile 调用了带有设置为 “this” 的上下文对象的回调函数
    //也就是原始的客户端的回调函数
 
    if (error) {
        return this(error);
    }
 
    // The try-catch block is needed to handle a possible syntax error from invalid JSON
    //try-catch块用于处理由于无效的JSON文件导致的可能出现的语法错误
 
    try {
        var json = JSON.parse(result);
        this(null, json);
    } catch (e) {
        this(e);
    }
 
}
 
function readFileAsJson(fileName, callback) {
    // The modified fs.readFile would take the context object as 4th argument.
    //修改后的 fs.readFile 会将这个上下文对象作为第四个参数
    // There is no need to create a separate plain object to contain `callback` so it
    //这里没有必要创建一个独立的 plain object 来包含 callback
    // can just be made the context object directly.
    //可以直接作为这个上下文对象
 
    fs.readFile(fileName, 'utf8', internalReadFileCallback, callback);
 
}

   

很明显,你需要同时控制 API 的两端,这使得这种优化方法对于不接受上下文对象参数的 API 不可行。然而,当你可以使用这种方法的时候(比如当你控制多个内部层的时候),这会带来非常大的性能提升。还有一个已知的事实:像 Array.prototype.forEach 这样的一些内建的 JavaScript 数组 API 可以接受一个上下文对象作为第二个参数。


2. 对象大小最小化

最小化那些经常并大量分配的对象(如 promises)的大小是至关重要的。在最常用的 JavaScript 实现中,对象分配的堆被分成不同的段和空间。较小的对象比较大的对象需要更多的时间来填满这些空间和段,从而减少了垃圾回收器的工作量。在决定对象有效或失效时,较小的对象可以使垃圾回收器访问更少的字段。

布尔和/或受限制的整数字段时,可以通过位运算符打包到一个更小的空间。JavaScript 位运算符可以操作 32 位的整数,因此你可以将 32 个布尔字段或 8 个 4 位的整数或 16 个布尔值和 2 个 8 位的整数等合并到一个字段中。为了保持代码的可读性,每一个逻辑字段都应该有一对 getter 和 setter 函数来执行物理字段的正确的位操作。将一个布尔字段打包为一个整数(未来可以扩大到容纳更多的逻辑字段)的例子如下:


// Use 1 << 1 for the second bit, 1 << 2 for the third bit etc.
 
const READONLY = 1 << 0;
 
class File {
    constructor() {
        this._bitField = 0;
    }
 
    isReadOnly() {
        // Parentheses are required.
        //括号是必须的
        return (this._bitField & READONLY) !== 0;
    }
 
    setReadOnly() {
        this._bitField = this._bitField | READONLY;
    }
 
    unsetReadOnly() {
        this._bitField = this._bitField & (~READONLY);
    }
 
}

   

这个存取方法非常短,因此它们在运行时很有可能被内联,从而不涉及函数调用的开销。

当使用一个布尔值来追踪这个字段保存的是哪种类型的值的时候,两个或多个从来不会同时使用的字段就可以压缩为一个字段。然而,像之前描述的办法一样,将这个布尔字段作为一个打包好的整数字段来执行仅仅是节约了一些空间而已。

在 Bulebird 中,这个技巧被用来储存 promise 执行的返回值或拒绝原因。这里并没有明显的分界:如果 promise 执行成功,那么这个返回值可能会被存储在失败的回调函数的区域;请求失败时,拒绝请求的原因也可能存储在成功的回调函数区域。再重复一次,所有的访问都应该通过存取函数来隐藏这些难看的优化细节。

如果一个对象请求了一系列值,你可以通过将这些值直接储存在对象的索引中,从而避免一个单独的数组分配。

所以,不要这样:


class EventEmitter {
 
    constructor() {
 
        this.listeners = [];
 
    }
 
    addListener(fn) {
 
        this.listeners.push(fn);
 
    }
 
}

   

你可以避免使用数组:

  
class EventEmitter {
 
    constructor() {
 
        this.length = 0;
 
    }
 
    addListener(fn) {
 
        var index = this.length;
 
        this.length++;
 
        this[index] = fn;
 
    }
 
}

   

如果 .length 可以被限制到一个很小的整数(比如 10 位,这将会把事件发射器限制为最多 1024 个监听器),那么它就可以和其他的受限制的整数或布尔值一起压缩成为一个打包好的字段。


3. 使用空函数并简单地覆盖它们来实现代价昂贵的可选功能

Bluebird 有一些可选的功能特性在使用时会导致整个库的统一的性能损失。这些功能是:警告,长的栈追踪,取消操作,Promise.prototype.bind 和 promise 状态监控。这些功能会在库中调用钩子函数(hook functions)。举个例子,每当创建一个 promise 对象的时候,promise 监控功能都要被调用一次。

比起不管监控功能是否启用都调用钩子函数,在调用前先检查一下监控功能是否启用要好得多。然而,由于内置缓存和函数内联,对于不启用该功能的用户,实际上可以完全消除成本。这可以通过将原始的 hook 方法设置为空操作函数来实现:


class Promise {
 
    // ...
 
    constructor(executor) {
 
        // ...
 
        this._promiseCreatedHook();
 
    }
 
    // Just an empty no-op method.
 
    _promiseCreatedHook() {}
 
}

   

现在,如果用户没有启用监控功能,优化器就会发现这个函数调用不执行任何操作,并将其消除。所以对 constructor 中 hook 方法的有效调用并不存在。

为了使这个功能真正地执行,启用该功能时必须用真正的实现覆盖所有相关的空操作函数:

function enableMonitoringFeature() {
 
    Promise.prototype._promiseCreatedHook = function() {
 
        // Actual implementation here
 
    };
 
    // ...
 
}
 

像这样的覆盖方法将会使所有 Promise 类的对象建立的内置缓存失效,因此只应该在启动应用程序时,在所有 promise 对象创建以前执行。如果在任一内存使用之前发生了覆盖,那么在功能被启用后的操作创建的内置缓存也不会识别到这些空操作函数的存在。

 

本文由职坐标整理发布,欢迎关注职坐标WEB前端JavaScript频道,获取更多JavaScript知识!

本文由 @沉沙 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved