资讯专栏INFORMATION COLUMN

通过一个场景实例 了解前端处理大数据的无限可能

willin / 968人阅读

摘要:随着前端的飞速发展,在浏览器端完成复杂的计算,支配并处理大量数据已经屡见不鲜。作为该实例本身的数据层。后人常以阿喀琉斯之踵譬喻这样一个道理即使是再强大的英雄,他也有致命的死穴或软肋。

随着前端的飞速发展,在浏览器端完成复杂的计算,支配并处理大量数据已经屡见不鲜。那么,如何在最小化内存消耗的前提下,高效优雅地完成复杂场景的处理,越来越考验开发者功力,也直接决定了程序的性能。

本文展现了一个完全在控制台就能模拟体验的实例,通过一步步优化,实现了生产并操控1000000(百万级别)个对象的场景。

导读:这篇文章涉及到 javascript 中 数组各种操作、原型原型链、ES6、classes 继承、设计模式、控制台分析 等内容。要求阅读者具有 js 面向对象扎实的基础知识。如果你是初级前端开发者,很容易被较为复杂的逻辑绕的云里雾里,“从入门到放弃”,不过建议先收藏。如果你是“老司机”,本文提供的解决思路希望对你有所启发,抛砖引玉。

场景和初级感知

具体来说,我们需要一个构造函数,或者说类似 factory 模式,实例化1000000个以上对象实例。

先来感知一下具体实现:

Step1

打开你的浏览器控制台,仔细观察并复制粘贴以下代码,触发执行。

a = new Array(1e6).fill(0);

我们创建了一个长度为1000000的数组,数组的每一项元素都为0。

Step2

在数组 a 的基础上,再生产一个长度为1000000的数组 b,数组的每一项元素都是一个普通 javascript object,拥有 id 属性,并且其 id 属性值为其在元素中的 index 值;

b = a.map((val, ix) => ({id: ix}))
Step3

接下来,在 b 的基础上,再生产一个长度为1000000的数组 c ,类似于
b,同时我们增加一些其它属性,使得数组元素对象更加复杂一些:

c = a.map((val, ix) => ({id: ix, shape: "square", size: 10.5, color: "green"}))

语义上,我们可以更直观的理解:c 就是包含了1000000个元素的数组,每一项都是一个绿色的、size 为10.5的小方块。

如果你按照指示做了下来,控制台上会有以下内容:

深层探究

你也许会想,这么大的数据量,内存占用会是什么样的情况呢?

好,我来带你看看,点击控制台 Profiles,选择 Take Shapshot。在Window->Window 目录下,根据内存进行筛选,你会得到:

很明显,我们看到:

a数组:8MB;

b数组:40MB;

c数组:64MB

也许在实际场景中,除了1000000个绿色的、size为10.5的小方块,我们还需要很多不同颜色,不同 size 的形状。之前,这样“{{BANNED}}”的需求常见于游戏应用中。但是现在,复杂项目中类似场景,也许距离你并不遥远。

ES6 Classes处理需求

简单“热身”之后,我们了解了实际需求。接下来,我们考察一下 ES6 Classes 处理这个问题的情况。请重新刷新浏览器 tab,复制执行以下代码。

class Shape {
    constructor (id, shape = "square", size = 10.5, color = "green") {
        this.x = x; //  坐标x轴
        this.y = y; //  坐标y轴
        Object.assign(this, {id, shape, size, color})
    }
}

a = new Array(1e6).fill(0);
b = a.map((val, ix) => new Shape(ix));

我们使用了ES6 Classes,并扩充了每个形状的坐标信息。
此时,再来看一下内存占用情况:

很明显,此时 b 数组由1000000个形状组成,占据内存:80MB,超过了先前数组的内存消耗。也许这并不出乎意料,此时的b数组毕竟又多了两个属性。

优化设计:Two-Headed Classes

我们先来分析一下上面的实现,熟悉原型链、原型概念的同学也许会明白,之前的方案产生的实例,顺着原型链上溯,具有三层原型属性:

第一层属性:[id, shape, size, color, x, y]; 这一层属性的 hasOwnproperty 为 true; 属性存在于实例本身。

第二层:[Shape]; 顺着原型链上溯,这一层 instance.__proto__ === Constructor.prototype; ( proto 左右两边 __ 被编辑器吃掉了,请见谅,下同)

第三层:[Object]; 这一层: instance.__proto__.__proto__ === Object.prototype; 如果在向上追溯,就为 null 了。

这样的情况下,实际业务数据层只有一层,即为第一层。

但是,请仔细思考,如果有大量的不同颜色,不同size,不同形状的情况下。单一数据层,是难以满足我们需求的。
我们需要,再添加一层数据层,构成所谓的 Two-Headed Classes!同时,还需要对于默认的属性,实现共享,以节省内存的占用。

什么什么?没听明白,那就请看具体操作吧。

### 如何实现?
我们可以使用 Object.create 方法,这样使得生产得到的实例的
proto 指向 b 数组的元素,然后在最顶层设计一个 id 属性。

也许这样说过于晦涩,那就直接参考代码吧,请注意,这是本篇文章最难以理解的地方,请务必仔细揣摩:

two = Object.create(b[0]); 
// two.__proto__ === b[0]
two.id = 1;

还记得 b 数组是什么嘛?参考上文,它由

b = a.map((val, ix) => new Shape(ix));

得到。

这样子的话,对于每一个实例,我们有如下关系:

第一层:[id]; 这一层实例的 hasOwnproperty 为 true;

第二层:[id, shape, size, color, x, y]; 这一层 instance.__proto__ === Constructor.prototype;

第三层:[Shape];

第四层:[Object]; 这一层的再顶层,就为null了。

我们将 Shape 的一个实例作为一个新的 object 的原型,并复写了 id 属性,原有的 id 属性将作为默认 id。

当然,上边的代码只是“个案”,我们进行“生产化”:

proto = new Shape(0);
function newTwoHeaded (ix) {
    const obj = Object.create(proto);
    obj.id = ix;
    return obj
}
c = a.map((val, ix) => newTwoHeaded(ix));

这么做多加入了一个数据层,那么有什么“收获”呢?我们来看一下b和c的内存占用情况吧:

这表明:我们从80MB的b,优化得到了64MB的c!
原因当然就在于虽然多加了一层原型结构,但是第二层变成了“共享”。

当然,如果到这里你还没有晕的话,可能要问:那第二层诸如 shape, size, color 这些属性变成共享的之后,存在互相干扰怎么破解呢?

好问题,我先不解答,先给大家看一下最后的final product:

class ShapeMaker {
    constructor () {
        Object.assign(this, ShapeMaker.defaults())
    }
    static defaults () {
        return {
            id: null,
            x: 0,
            y: 0,
            shape: "square",
            size: 0.5,
            color: "red",
            strokeColor: "yellow",
            hidden: false,
            label: null,
            labelOffset: [0, 0],
            labelFont: "10px sans-serif",
            labelColor: "black"
        }
    }
    newShape (id, x, y) {
        const obj = Object.create(this);
        return Object.assign(obj, {id, x, y})
    }
    setDefault (name, value) {
        this[name] = value;
    }
    getDefault (name) {
        return this[name]
    }
}

在实例化的时候,我们便可以这样使用:

shapeProto = new ShapreMaker();
d = a.map((val, ix) => shapeProto.newShape(ix, ix/10, -ix/10))

就像上面所说的,初始化实例时,我们初始化了 id, x, y 这么三个参数。作为该实例本身的数据层。这个实例的原型上,也有类似的参数,来保证默认值。这些原型上的属性,对于实例数组中的每个实例,都是共享的。

为了更好的对比,如果设计是这样子:

function fatShape (id, x, y) {
    const a = new shapeMaker();
    return Object.assign(a, {id, x, y})
}
e = a.map((val, ix) => fatShape(ix, ix/10, -ix/10))

那么所有属性无法共享,而是各自拷贝了一份。在内存的占用上,将是我们给出方案的三倍之多!

阿喀琉斯之踵

阿喀琉斯,是凡人珀琉斯和美貌仙女忒提斯的宝贝儿子。忒提斯为了让儿子炼成“金钟罩”,在他刚出生时就将其倒提着浸进冥河,遗憾的是,乖儿被母亲捏住的脚后跟却不慎露在水外,全身留下了惟一一处“死穴”。后来,阿喀琉斯被帕里斯一箭射中了脚踝而死去。
后人常以“阿喀琉斯之踵”譬喻这样一个道理:即使是再强大的英雄,他也有致命的死穴或软肋。

就像我们刚才提的到解决方案一样,也有一些“不足”。问题其实在之前我也已经抛出:“第二层诸如:shape, size, color 这些属性变成共享的之后,存在互相干扰怎么破解呢?”

这个问题的答案其实也隐藏在上面的代码中,很简单,就是我们在实例的自身属性上,进行复写,而避免更改原型上的属性造成污染。

如果你看的云里雾里,不要紧,马上看一下我下面的代码说明:

d.every((item) => item.shape === "square") // true

打印为 true,是因为 d 数组中的每个实例的 shape 属性,都在原型上,且初始值都为"square";

现在我们调用 setDefault 方法,实现对默认 shape 的改写。

shapeProto.setDefault("shape", "circle");
d.every((item) => item.shape === "square"); // false

因为此时所有实例的 shape 都在原型上,并共享这个原型。更改之后,我们有:

d.every((item) => item.shape === "circle"); // true

但是,我只想把第一个实例的 shape 设置为 triangle,其他的不变,该怎么办呢?只需要在第一个实例上,增加一个 shape 属性,进行重写:

d[0].shape = "triangle";
d.every((item) => item.shape === "circle"); // false

好吧,尝试完毕之后,我们在变回来。

d[0].shape = "circle";

这时候,自然有:

d.every((item) => item.shape === "circle"); // true

同时,再折腾一下:

d[0].shape = "triangle";
d.every((item) => item.shape === "triangle"); // false

相信下面的也不难理解了:

shapeProto.setDefault("shape", "triangle");
d.every((item) => item.shape === "triangle"); // true

这种模式其实比单纯使用ES6 Classes要灵活的多,同时也节省了内存。所有的静态属性都是共享的,但是共享的静态属性又都是可变的,可复写的。

总结

这篇文章,我们在开头部分了解到了在大量数据的情况下,内存的占用是如何一步一步变的沉重。同时,我们提供了一种,在传统的
Classes 之上增加一个数据层的方法,有效地解决了这个问题。解决方案充分利用了 Object.create 等手段。

当然,理解这些内容并不简单,需要读者有比较扎实的 javascript 基础。在您阅读过程当中,有任何问题,欢迎与我讨论。

内容借鉴了Owen Densmore最新文章:Two Headed ES6 Classes!,喜欢看英文原版的同学可以直接戳链接。中文翻译版并非直译,进行了较大幅度的讲解和增删。

Happy Coding!

PS:
作者Github仓库 和 知乎问答链接
欢迎各种形式交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84001.html

相关文章

  • 阿里云前端周刊 - 第 16 期

    摘要:或许,它还终将改变用户对移动的期待。通过一个场景实例了解前端处理大数据的无限可能随着前端的飞速发展,在浏览器端完成复杂的计算,支配并处理大量数据已经屡见不鲜。 推荐 1. 神经网络入门 http://www.ruanyifeng.com/blo... 眼下最热门的技术,绝对是人工智能,人工智能的底层模型是神经网络(neural network)。许多复杂的应用(比如模式识别、自动控制)...

    waruqi 评论0 收藏0
  • 2017-07-15 前端日报

    摘要:前端日报精选从设计到源码用强类型语言增强通过一个场景实例了解前端处理大数据的无限可能专题之从零实现的表单验证第一部分使用和技巧对表单进行约束验证中文译即将到来的正则表达式新特性掘金个快速编程技巧个人文章周刊第期相信控制像一样使用 2017-07-15 前端日报 精选 Redux从设计到源码用强类型语言GraphQL增强React通过一个场景实例 了解前端处理大数据的无限可能JavaSc...

    Vicky 评论0 收藏0
  • PHP小知识点

    摘要:那些琐碎的知识点作者记录的的很奇特很难记的知识点。易错知识点整理注意和的区别中和都是输出的作用,但是两者之间还是有细微的差别。今天手头不忙,总结一下,分享过程中掌握的知识点。 深入理解 PHP 之:Nginx 与 FPM 的工作机制 这篇文章从 Nginx 与 FPM 的工作机制出发,探讨配置背后的原理,让我们真正理解 Nginx 与 PHP 是如何协同工作的。 PHP 那些琐碎的知识...

    hover_lew 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<