Skip to content

Latest commit

 

History

History
164 lines (119 loc) · 6.63 KB

[07] 懒执行表与里.md

File metadata and controls

164 lines (119 loc) · 6.63 KB

懒执行表与里

这周浏览 github trending 无意间看到了 facebook 的一个叫做 immutable-js 的库,“不可变”虽然表面看上去和“懒执行”没有什么交集,但事实上内在关系非常大。

现在纯粹的懒执行库里,比较有名的是 lazy.js,如果你有兴趣,可以先看看它的介绍,或者 wiki 相关概念。

动机

不多说废话,这里举个例子。比如有这样一个需求,A 同学现在有一些待处理的 url 数据在变量 url_list 里,A 需要使用 B 同学写的 map_url_list 函数处理这些数据,代码可能会写成这样:

var urls = map_url_list(url_list);

// 每点击一下,处理一个 url
$('#btn').click(function () {
    var url = urls.pop();
    // 处理 url...
});

功能解耦的好处是 A 不需要关心 B 怎么实现 map_url_list 函数的,A 只管调用即可。我们假设一下 B 的需求,map_url_list 函数可能长这样:

var map_url_list = function (url_list) {
    return url_list.map(function (url, i) {
        return url + $('.url-' + i).length;
    });
}

上面这种实现可以说是最正常的想法,功能上来讲确实是无懈可击。但是 B 不知道的是,A 现在突然要处理 10 万条数据。由于这个函数需要循环 10 万次,这很可能让浏览器无法响应一会儿,直接影响用户体验。

当然坏处不止这一点,这个函数还产生了一个新的拥有 10 万条目的数组。虽然 V8 用了很多技巧将新字符串和旧字符串关联起来,没有耗费太多内存,但站在上层我们依然应该尽量精益求精。

表解决方案

这里开始就引入懒执行的具体概念了,我总结了下:

在需要的时候才处理数据,而不是提前准备好数据。

按照这个想法,我们作为 B,应该优化一下自己的函数,可以这样写:

var map_url_list = function (url_list) {
    var i = 0;
    return {
        pop: function () {
            var url = url_list[i++];
            return url + $('.url-' + i).length;
        }
    };
}

相比之前的函数,这个函数运行完毕,没有产生一次循环,瞬间就执行完了。A 在调用 urls.pop() 的时候,才会运行函数,换句话说只在用户点击目标的时候才运行一次,否则直到用户离开页面,或许一次多余的运算也没发生。

里思考

看上去这个函数很好的满足了需求,性能大概比之前那个快了 10 万倍,听上去很不错,但实际运用起来,像之前那么设计会出现很多问题。举个最简单的例子。如果 A 的代码是这样的:

var urls = map_url_list(url_list);

// 每点击一下,处理一个 url
$('#btn').click(function () {
    var url = urls.pop();
    // 处理 url...
});

// 每点击一下,增加一个 url
$('.add').click(function () {
    // 当然这里用 push 的话,反而用懒执行可能会更好,这也是懒执行作为抽象层的一大好处,
    // 能更好的处理潜在的无穷型数据结构。
    url_list.unshift(this.dataset.url);
});

// 每点击一下,删除最后一个 url
$('.del').click(function () {
    url_list.pop();
});

如果 url_list 是一个动态的值,那么 map_url_list 作为一个封闭的环境,在 #btn 被点击一下之后点击 .add,然后再点击 #btn,那 urls.pop() 返回的值将依然是上次的值。所以并不是任何情况下懒执行都能很好的适用:

懒执行通常需要通过牺牲安全性来提升性能。

为什么说是安全性呢?其实这个有点仁者见仁智者见智了,好比 clone 一份数组和直接依赖这个数组,通常是 clone 一份再处理更安全。数据依赖和不安定总结伴而行。

不可变的数据源

针对上面这种问题,利用不可变数据将会鼓励开发者从不同的角度思考程序中数据的流向。这里我翻译的 immutable-js 原话:

Developing with immutable data encourages you to think differently about how data flows through your application.

这个想法来源于 facebook 之前提出的 Flux 设计模式,里面的演讲非常有意思,很值得扩展阅读。 如果数据源是不可变的我们要处理的问题往往会变得容易很多。这里举个例子来表达这个概念的基本想法。还是刚才那个问题,合并了 A 和 B 的代码的完整实现:

/**************************** B 同学的代码部分 *****************************/

// 这个不可变数据结构只提供演示作用的 unshift 和 pop 功能。
// 这是一个典型的只具备入列和出列功能的单向队列数据结构。
var immutable = function (arr) {
    var _arr = [];
    var bound = arr.length;
    var immutable_arr = {
        unshift: function (item) {
            _arr.unshift(item);
        },
        pop: function () {
            if (--bound < 0)
                return _arr.pop();
            else
                return arr[bound];
        }
    };
    return immutable_arr;
};

var map_url_list = function (url_list) {
    var i = 0;

    return {
        pop: function () {
            var url = url_list[i++];
            return url + $('.url-' + i).length;
        }
    };
}

/**************************** A 同学的代码部分 *****************************/

var urls = map_url_list(url_list);

// 这个步骤将 url_list 转变为了一个不可变对象。
// 之后对它的 pop、unshift 操作将不会影响到 map_url_list 的工作。
im_url_list = immutable(url_list);

$('#btn').click(function () {
    var url = urls.pop();
    // 处理 url...
});

$('.add').click(function () {
    im_url_list.unshift(this.dataset.url);
});

$('.del').click(function () {
    im_url_list.pop();
});

可以看到通过实现一个特殊的 单向队列 数据结构,我们很好地将只读数据和可变数据分开了,并且我们没有浪费多余的内存。

但如果我们将 im_url_list.unshift(this.dataset.url); 改为 im_url_list.pop(this.dataset.url);,那么这是使用 不可变对象 可能就不像预期那样好用了,但懒执行依然适用。可见 二者不能相互替代,你需要根据实际情况选择用哪一个。

总结

讲了这么多,并不是想推荐大家使用什么 js 库,更多时候你需要的是了解设计方法,以及如何用自己的大脑去思考。常常只需要灵活变化一下现有的设计方法,而不是依赖一个庞大的函数库,就能非常好的解决实际问题。