• JavaScript Promises


    上篇文章介绍了JavaScript异步机制,请看这里

    JavaScript异步机制带来的问题

    JavaScript异步机制的主要目的是处理非阻塞,在交互的过程中,会需要一些IO操作(比如Ajax请求,文件加载,Node.js中的文件读取等),如果这些操作是同步的,就会阻塞其它操作。

    异步机制虽然带来了许多好处,但同时也存在一些不如意的地方。

    代码可读性

    这样的代码读起来简直累觉不爱啊~~~

    operation1(function(err, result) {  
        operation2(function(err, result) {  
            operation3(function(err, result) {  
                operation4(function(err, result) {  
                    operation5(function(err, result) {  
                        // do sth. 
                    })  
                })  
            })  
        })  
    })

    流程控制

    异步机制使得流程控制变的有些困难,比如,在N个for循环中的回调函数执行完成之后再做某些事情:

    var data = [];
    fs.readdir(path, function (err, files) {
        if (err) {
            console.log(err)
        } else {
            for (var i in files) {
                (function (i) {
                    fs.stat(path + '/' + files[i], function (err, stats) {
                        if (err) {
                            console.log(err)
                        } else {
                            o = {
                                "fileName": files[i].slice(0, -5),
                                "fileTime": stats.mtime.toString().slice(4, 15)
                            };
                            data.push(o);
                        }
                    })
                })(i);
            }
        }
    });
    var html = template('templates/main', data);
    res.writeHead(200, {'Content-Type': 'text/html; charset="UTF-8"'});
    res.write(html);
    res.end();

    上面的代码不能获得预期的结果,因为for循环中所有的fs.stat执行结束后,data才会获得预期的值。可是,怎么知道for循环全部执行结束了呢?

    异常处理

    再看看上面的代码,如果多几个需要处理异常的地方,代码可谓支离破碎了。

    Promises

    Promise是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。

    Promises全称叫Promises/A+,是一个开放的JavaScript规范,已经被加入ES6中。Promises只是一种规范,从Chrome32起开始支持Promises。

    已经实现这个标准的库有Q、when.js、Dojo.deferred、jQuery.deferred等。

    鉴于浏览器已经支持Promises,所以尽量使用原生的Promises,对于不支持Promises的浏览器,建议使用这个polyfill: promise-5.0.0.js,Promises其实很简单:

    function a (num) {
        var promise = new Promise(function (resolve, reject) {
            var count = num;
            if (count >= 10) {
                setTimeout(function () {
                    count ++;
                    console.log(count);
                    resolve(count);
                }, 1000);
            } else {
                reject(count);
            }
        });
        return promise;
    }
    function b (count) {
        var promise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                count *= 10;
                console.log(count);
                resolve(count);
            }, 1000);
        });
        return promise;
    }
    function c () {
        console.log('done');
    }
    function alert(num) {
        console.log('您输入的数字为' + num + ',不能小于10!');
    }
    a(11)
    .then(b, alert)
    .then(c);

    上面这段代码做的事情是:输入num,1s后输出num + 1,2s后输出(num + 1) * 10,最后输出done。

    相比a、b两个方法本身,通过Promises定义的方法多了这些东西:

    定义一个promise。

    var promise = new Promise(function (resolve, reject) {));

    完成方法的功能时,调用resolve(data)。

    resolve(count);

    最后返回promise。

    return promise;

    首先熟悉一下Promises是怎样定义promise状态的。Promises/A+是这样规定的:

    • 一个promise必须是下面三种状态之一:pending, fulfilled, rejected
    • 当一个promise是pending状态:
      • 可以转变到fulfilled状态或者rejected状态
    • 当一个promise是fulfilled状态:
      • 不可以转变到其他任何状态
      • 必须有一个不能改变的value
    • 当一个promise是rejected状态:
      • 不可以转变到其他任何状态
      • 必须有一个不可改变的reason

    上面代码在a()中使用new Promise()实例化了一个promise后,这个promise默认状态是pending。

    promise.then()方法有2个参数,分别是onFulfilled、onRejected,这2个参数都是方法名(也就是回调函数),通过这2个参数可以对应promise的fulfilled和rejected2种状态。

    通过上面的代码来解释就是:

    • 当a()正常执行结束时,调用resolve(data)将promise的状态改变为fulfilled,并且通过then()的第一个参数onFulfilled将参数data传递给下一个方法b()。

    • 当a()非正常结束,这里认为a()在执行过程中出现了异常时,调用reject(reason)将promise的状态改变为rejected,并且通过then()的第二个参数onRejected将参数reason传递给下一个方法alert()。

    在前面说到流程控制的时候提到的for循环的问题还没有解决:如果想等N个for循环中的回调函数执行结束之后做某些事情,该怎么办?

    这时候该用到Promise.all()方法了,比如前面提到的一段代码,需求是这样:

    在Node.js中,创建http服务器,读取当前目录下articles目录中的所有文件,遍历所有文件,并根据“目录+文件名”读取文件的最后修改时间,最终返回[{文件名,文件修改时间}, {文件名,文件修改时间}, ...]这样一个列表到客户端。

    这里存在的问题是,读取目录的操作是异步的,for循环读取文件状态的操作也是异步的,而在for循环中的所有异步操作都执行结束后,需要调用response.writeHead()与response.write()将所有异步数据返回到客户端。在使用when.js之前,我能想到的就是把for循环中的异步操作变为同步操作,最后再返回数据,但是就会阻塞其他的同步操作,显然这违背了异步机制。

    利用Promise.all()改造过后的代码:

    var http = require('http'),
        fs =require('fs'),
        connect = require('connect'),
        Promise = require('promise');
    function readDir (path) {
        return new Promise(function (resolve, reject) {
            fs.readdir(path, function (err, files) {
                if (err) {
                    reject(err);
                } else {
                    resolve({
                        "path": path,
                        "files": files
                    });
                }
            });
        });
    }
    function getFileStats (data) {
        var
            files = data.files,
            promiseList = [];
        for (var i in files) {
            (function (i) {
                var promise = new Promise(function (resolve, reject) {
                    fs.stat(data.path + '/' + files[i], function (err, stats) {
                        if (err) {
                            reject(err)
                        } else {
                            var o = {
                                "fileName": files[i].slice(0, -5),
                                "fileTime": stats.mtime.toString().slice(4, 15)
                            };
                            resolve(o);
                        }
                    })
                });
                promiseList.push(promise);
            })(i);
        }
        return Promise.all(promiseList);
    }
    var app = connect()
        .use(function(req, res) {
            if (req.url === '/favicon.ico') {
                return;
            } else {
                readDir('articles/fe').then(getFileStats)
                .then(function (o) {
                        res.writeHead(200, {'Content-Type': 'text/html; charset="UTF-8"'});
                        res.write(JSON.stringify(o));
                        res.end();
                    });
            }
        })
        .listen(8080);

    浏览器上的结果:

    Promise.all(array)需要传入一个promise数组,其中数组中的每一个promise在fulfill时都会执行resolve(data),这里的data就是前面for循环中每一次异步操作中获得的数据。在promise.all()执行过后,会将每次resolve(data)中的data拼成一个数组,通过then()传递给下一个promise。

    Promise.race()

    Promise.race()为异步任务提供了竞争机制。比如在N个异步任务中,在最快获得结果的任务之后做某些事情,可以使用Promise.race()。

    使用Promise.race()同Promise.all()类似,传入的参数都是promise数组,返回promise数组中最早fulfill的promise,或者返回最早reject的promise。

    function a () {
        var promise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                resolve('a');
            }, 1002);
        });
        return promise;
    }
    function b () {
        var promise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                resolve('b');
            }, 1001);
        });
        return promise;
    }
    function c (data) {
        console.log(data + ' first!');
    }
    Promise.race([a(), b()]).then(c);

    执行结果:b first!

  • 相关阅读:
    第一次作业
    Zotero引用文献格式(软件学报)
    好文索引
    【思维导图】分享以前整理的两张思维导图吧
    Nokia N9开启开发者模式
    Windows10 Ubuntu子系统折腾
    [CSAPP]并发与并行
    [读书笔记]逻辑思维,只要五步
    linux配置备忘
    从现在就开始写博客
  • 原文地址:https://www.cnblogs.com/zhaodongyu/p/3933486.html
Copyright © 2020-2023  润新知