Promise vs Observable

Posted by YanYang Yu on May 16, 2016

前言

学习Angular2的过程当中,发现Angular2并没有沿用1当中的$q,继续使用Promise来处理异步流程,转而拥抱了Rxjs的Observable。 为什么Angular2的开发团队作出了这样的选择?这要对Promise和Observable适用场景作出比较。

Promise/A+

Promise通过链式调用让我们能够以同步的形式编写异步流程,当然了要多写一些额外的代码,例如

promise.then(getData).then(getNextData).then(getNextNextData).catch(fail)...

基本上就是不停的添加then,额外的代码多多少少会影响整个逻辑的可读性,没有格式化的代码就更难阅读和理解了。 Promise/A+是在Promise的基础上作出的一些规范和约定,详细的内容见这里。 Promise主要是通过内部缓存和函数队列来实现的,这里有一个详细的教程,有兴趣的可以看下。 目前大部分主流浏览器都已经支持Promise了

alt

上面我说额外的代码会影响可读性,这都不是重点,我们可以通过优雅的代码组织来提高可读性,但是Promise的设计缺陷就不是那么好规避的了。

延迟

图片懒加载相信大家都很熟悉,滚动到特定区域才会加载该区域的图片。这样可以大量减少HTTP的请求数目,有效的提高首屏的加载速度。 那么Promise是不是也会等到我们添加then的时候才会执行呢?看个例子

var promise = new Promise((resolve,reject)=> {
  console.log('promise exec');
  resolve(5);
});

我们没有添加then,控制台仍然打印了promise exec,这说明Promise是立即执行的,跟你添不添加链式调用没有任何关系。 实际上Promise调用resolve的时候,内部缓存了这个值,当我们添加调用then的时候,将这个值传递给函数并执行。 如果我们先添加的then后执行的resolve,那么Promise内部会缓存then添加的函数,等到resolve执行的时候,在将参数传递给之前缓存的函数并执行。 所以说,Promise不存在什么延迟执行。当然了,基本上不存在调用Promise而不添加then的情况,一般我们都是这样使用Promise的

function getProdList(){
  return new Promise((resolve,reject)=> {
    // http
  });
}

getProdList().then(res => {
  // handle res
});

取消

看个简单的例子:点击按钮,向后端发起一个请求

function getProdList() {
    return new Promise((resolve, reject)=> {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = ()=> {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(xhr.responseText);
                } else {
                    reject(xhr.responseText);
                }
            }
        };

        xhr.open('GET', 'package.json', true);
        xhr.send();
    });
}

$fetch.click(()=> {
    getProdList().then(res => {
        console.log(res);
    });
});

现在需求变更,添加一个取消按钮,点击后取消发送

$cancel.click(()=> {
    // todo
});

不知道怎么去编码了,因为Promise压根就没有提供这样的接口,我们也没办法拿到内部的xhr调用xhr.abort()

重试

接着上面的例子,请求数据失败后在重试两次。不好意思Promise不提供这样的功能,需要我们自己开发

function retry(fn, times) {
    return fn().catch(err => {
        if (times <= 0) return Promise.reject(err);
        return retry(fn, times--);
    });
}

retry(getProdList, 2);

小结

实际上开发中,我们很少有上述的需求,一是网络够快,没还点取消结果已经返回了。 而且请求很少有失败的需要重试,只有像淘宝这种访问量比较大,首屏加载需要重试机制,但是背后需要大量服务器的支持。 Promise基本能满足我们开发的需求,但是可控性太差,这还远远不够。

Observable

RxJS(Reactive Extensions for javascript),响应式的思路是把随时间不断变化的数据、状态、事件等等转成可被观察的序列(Observable Sequence),然后订阅序列中那些Observable对象的变化。

alt

Observable可以同我们数组操作进行类比,例如有这样一个数组

var arr = [1, 2, 3, 4];

我们将每个元素平方,然后过滤掉大于10的数据

var newArr = arr.map(x => x * x).filter(x => x <= 10);

我们来看看Observable是怎么处理的

Rx.Observable.from(arr)
        .map(x => x * x)
        .filter(x => x <= 10)
        .subscribe(x => console.log(x));

是不是跟数组的操作很像,异步操作同样可以当做序列

Rx.Observable.fromEvent($fetch, 'click')
        .subscribe(e => console.log(e.target === $fetch[0]));

这里有个视频,比较详细的介绍了什么是RxJS。 使用Observable可以很好的解决Promise的短板,Observable是可控的。

延迟

var source = Rx.Observable.create(observer=> {
    console.log('observable exec');
    observer.onNext(5);
    observer.onCompleted();
});

我们创建了一个Observable,但是没有添加订阅,这时候控制台没有输出任何内容,现在我们添加上订阅

source.subscribe(x => console.log(x));

打开控制台你会发现输出了相应的内容,通过这个例子可以说明Observable是延迟执行的。

取消

拿Promise的例子修改

var source = Rx.Observable.create(observer=> {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = ()=> {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                observer.onNext(xhr.responseText);
            } else {
                observer.onError(new Error(xhr.responseText));
            }
            observer.onCompleted();
        }
    };

    xhr.open('GET', 'package.json', true);
    xhr.send();

    return function () {
        xhr.abort();
    }
});

var subscription;

Rx.Observable.fromEvent($fetch, 'click')
        .subscribe(e => {
            subscription = source.subscribe(
                    res => console.log(res),
                    err => console.log(err)
            );
        });

Rx.Observable.fromEvent($cancel, 'click')
        .subscribe(e => {
            if (subscription) {
                subscription.dispose();
            }
        });

重试

同样拿Promise的例子修改

var source = Rx.Observable.create(observer=> {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = ()=> {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                observer.onNext(xhr.responseText);
            } else {
                observer.onError(new Error(xhr.responseText));
            }
            observer.onCompleted();
        }
    };

    xhr.open('GET', 'package.gjson', true);
    xhr.send();

    return function () {
        xhr.abort();
    }
}).retry(3);

添加.retry(3)即可,为什么是3,因为这里包括本身发送的一次。 Observable有很多操作API,参见这里

小结

Rx.Observable提供了丰富的API供我们使用,十分方便。但是有时候写法会比较复杂,例如事件绑定

Rx.Observable.fromEvent($fetch, 'click')
        .subscribe(e => {

        });

RxJS是函数式编程的衍生物,所以我们要以函数式编程的思想去看待,不要想着什么OOP了。

函数式编程

函数式编程是一种编程范式,指用函数嵌套来操作我们的输入输出,函数也可以作为参数和返回值。这里的函数是指纯函数:相同的输入获取到相同的输出。例如

function add(x, y){
    return x + y;
}

add函数就是一个纯函数,固定的输入获得固定的输出,如add(1,2)输出永远是3。

function random(x){
    return x + Math.random();
}

random函数就不是纯函数,因为输出是随机的。

组合

Observable的链式调用实际上运用了函数式编程当中的组合:通过函数的结合产生新的函数

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

当然也可以添加多个参数

var compose = function(e,f,g) {
  return function(x) {
    return e(f(g(x)));
  };
};

ramda是一个javascript函数式编程库,提供了如curry、compose等等方法。 上面先map后filter的例子利用ramda改成函数式写法

function pow2(x) {
    return x * x;
}

function lt10(x) {
    return x <= 10;
}

var resultFn = R.compose(R.filter(lt10), R.map(pow2));
resultFn(arr);

是不是觉得很难理解,换成Observable的链式调用对我们来说会更容易理解点。函数式编程博大精深,我也是处于一知半解的状态。

总结

接触的越多越无知,学海无涯啊!