03月17, 2018

Decoratorfy

很久没提笔,先从简单的写。老早就了解过decorator,当时就觉得这个这特性很好,但是当你真的开始使用的时候,还是会感叹,妙哉妙哉。

本文主要想讲的是decoratorfy,将高级函数快速应用于decorator,代码只有几行,欢迎吐槽交流,大神请移步结果

从高阶函数说起

简单的说,就是输入有函数,输出是根据参数生成的一个新的函数。这样就很有意思了,先来看小例子——run

function autoCache(fn) {
    let cache = null;
    return function(...args) {
        if (cache) {
            return cache;
        } else {
            cache = fn.apply(this || {}, args);
            return cache;
        }
    }
}
const big = autoCache(() => {
    console.log('calculate');
    return Math.pow(3, 12);
});
const getData = autoCache(() => {
    console.log('fetch');
    return new Promise((resolve) => {
        setTimeout(resolve.bind(null, 'result'), 1000);
    });
});
console.log(big()); // calculate 531441
console.log(big()); // 531441
getData().then(console.log); // fetch result
getData().then(console.log); // result
// 只会有一次的calculate,一次的fetch。

这个简单的autoCache函数(PS:这个函数在实际使用中可能要多做一些错误处理),却大有用处。超复杂的运算,有时候为了体现逻辑,或者方便改代码,会把公式列出来。如果写在函数里,多次获取值就要多次运算,但是通过缓存就可以做到一次求值,并且是惰性求值,完美。可以缓存的ajax请求,读写文件等异步操作函数,这样缓存下来,没有多余的请求操作,不会重复读写文件,最后能拿到相同的结果。

像这样的高阶函数,常见的还有,lodash.debouncelodash.throttle等。高阶函数的作用大概就是将函数的通用行为抽象出来,当你要用这类行为的时候就不用做重复处理。

在一个{}里面用高阶函数可能像就是这样

const obj = {
    getSum: autoCache(function getSum() {
       return Math.pow(3, 12);
    }),
}

这样的方式看着有点别扭,写起来也相对麻烦,主要因为要嵌套吧。所以在ES新提案中class中优化了这个写法,也就是将要出场的decorator,事实上decorator要强大的多。

decorator

Decorator装饰器不仅可以装饰函数function,还可以装饰类class。装饰类的情况,比如从React

class MyComponent extends React.Component {
}

angular

@Component({...options})
class MyComponent {
}

且不说谁好,至少后者会更灵活。不过装饰类不是本文的重点,我们还是回到装饰函数上。先来看一下装饰器函数怎么用把,大概长下面这样。

class MyClass {
    @autoCache
    getSum() {
        console.log('calculate');
       return Math.pow(3, 12);
    }
}

是不是看上去清爽多了。那么,作为一个这样的装饰器函数我们该怎么实现。autoCache会接收到3个参数

  1. target——类的原型对象,如上大概是MyClass.prototype
  2. name——该属性的名字, 如上大概是getSum
  3. descriptor——该属性的描述对象,额,这个。大概就是描述getSum的一个对象,属性有configurableenumerablevaluewritable。还不清楚的就翻一翻红宝书吧。 然后返回一个被修改后的descriptor

我们可以修改descriptor.value来实现函数的替换。实现如下

function autoCache(target, name, descriptor) {
    var oldValue = descriptor.value;
    let cache = null;
    descriptor.value = function(...args) {
        if (cache) {
            return cache;
        } else {
            cache = oldValue.apply(this || {}, args);
            return cache;
        }
    }
    return descriptor;
}

是不是很简单。是的,就是那么简单。再比如实现一个callOnce来限制函数只能掉用一次,是不是很简单。那如果需求变成了限制函数只能掉用两次,总不能再写一个callTwice。解决问题的办法还是高阶函数,我们来实现一个limitCall,如下。呃,严格来讲不算是高阶函数,说是一个函数工厂更合理一些。

function callLimit(limitCallCount = 1, level = 'warn') {
    let callCount = 0;
    return function (target, name, descriptor) {
        var oldValue = descriptor.value;
        descriptor.value = function(...args) {
            if (callCount < limitCallCount) {
                callCount++;
                return oldValue.apply(this || {}, args);
            }
            console[level] && console[level](name, 'call limit');
        }
    }
}

使用limitCall会更灵活一点。

class MyClass {
    @callLimit(1)
    init() {
    }
}

想限制多少次就限制多少次,妈妈再也不用担心我加班了。

如何理解这里的代码,@后面如果是函数掉用的话,不管你嵌套多少层函数掉用,只要最后返回的是一个装饰器函数(满足上面3个参数,返回一个descriptor)就可以。

本文只讲decorator的一些简单用法,更详细的用法和细节请参阅阮一峰老师的书

decoratorfy

再回头看一看autoCache可以是一个高阶函数,稍微改一改可以变成一个装饰器函数。callLimit给的是一个装饰器的形式,但是也可以是一个高阶函数,如下形式,具体实现就不拷贝过来占篇幅了,又没人给稿费是吧。

function callLimit(fn, limitCallCount = 1) {
}

这里我们其实也可以看到其中的通用行为,我们可以做一个抽象。如果一个高阶函数满足第一个参数是一个函数,后面的参数其他设置。我们可以用高阶函数把这个高阶函数转成装饰器函数(事实上大部分高阶函数满足这个)。

function decoratorify(fn, ...rest) {
    return function (target, name, descriptor) {
        var oldValue = descriptor.value;
        descriptor.value = fn.call(fn, oldValue, ...rest);
        return descriptor;
    }
}

代码很简单,有木有,这样我们可以很容易的将市面上已有的高阶函数拿来使用。像lodash.throttle,使用起来如下

import throttle from 'lodash.throttle';
class Logic {
    @decoratorify(throttle, 100)
    onScroll(evt) {
        // do something
    }
}

本文的代码讲的是思路,方便来理解,实际使用要处理很多特殊情况。可以使用core-decorators这个库。

(PS:最近写码的时候惯性的使用箭头函数,在高阶函数中返回的函数,要特别注意,非特殊情况不要用箭头函数)

祝大家撸码愉快

本文链接:http://crystalmiao.com/post/decorator.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。