内容


浅谈如何降低 HTML5 的异步编程复杂度

Comments

准备工作

假设一模拟场景,需求如下:

  • 从服务端获取员工列表数据(基本数据:员工 ID,姓名,年龄),并存于客户端;
  • 对于年龄在 30 岁以上的员工,获取其详细数据(基本数据及其入职时间),并存于客户端。

您可以根据自身经验,先评估用传统回调方式完成该需求的时间,然后和下文中的具体实现做比较。附件则为本文例子相关源码,使用技术主要有 SpringMVC + jQuery/Wind.js/JSDeferred + Web SQL(HTML5)等,基于 Maven 构建。

如果您对 HTML5 的 Web SQL 不是很熟悉,可以先阅读本站 HTML5 专题中的相关文章。

JavaScript 异步编程

对 JavaScript 异步编程概念不熟悉的读者可先阅读阮一峰的 JavaScript 异步编程的 4 种方法。引用他的话来说,JavaScript 执行环境是“单线程”。而所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。那么这种语言特性导致的结果就是:JavaScript 代码风格大多以回调写法为主。HTML4 规范下,您也许写过以下场景的代码:

  • 通过 Ajax 获取服务端数据;
  • 使用某个 UI 框架的 Dialog,定义某个按钮点击时的回调函数;
  • 实现某个动画。

等等,那么步入 HTML5 时代,您可能还会基于回调方式写一下场景的代码:

  • 实现离线存储功能;
  • 实现文件读写功能;
  • 基于 Worklight、PhoneGap 等框架提供的 JavaScript API 来使用移动设备资源。

日益种类繁多的回调式写法势必会给开发带来更多复杂度,下面将简单介绍几种改善异步编程体验的方式:

Promise 方式

Promise 规范

Promise 规范由 CommonJS 组织提出,目前还处于草案阶段。就设计目的简单的说,就是提供一个对象,通过该对象的方法,来代表某个异步操作的成功,失败等;现有草案(包含校订中的)主要有 5 个:Promises/APromises/BPromises/KISS、Promises/C(校订中)、Promises/D。即便该规范还不是标准,jQuery 1.5+、Dojo 1.7+、Node.js 中的 node-promise 等已经实现了 Promises/A 的规范,有些还提供了扩展功能,如中断操作,多个异步操作同时执行等;而 Node.js 则实现了 Promises/B 和 Promises/D。下面将主要介绍 Promises/A 以及 jQuery 的实现方式。

Promises/A 规范主要有以下几点:

  • 创建一个 promise 对象代表异步操作;
  • 该对象有三种状态:未完成(unfulfilled)、完成(fulfilled)、失败(failed);
  • 该对象提供一个名为 then 的方法,该方法有三个入参,依次能传入完成时的函数句柄(fulfilledHandler),失败时的函数句柄(errorHandler)以及获取当前进展的函数句柄(progressHandler);
  • then 方法会返回一个新的 promise 对象,这为链式(chain)操作提供便利。

jQuery 的实现

jQuery 从 1.5 起引入了 Deferred 类,实现了 Promises/A 规范,并提供一些增强功能,下面是一个简单的例子:如果获得一个偶数,异步操作成功,反之失败。希望通过这个例子能让读者对 Promise 规范有一个直观了解:

清单 1.jQuery 的 Deferred 类示例代码
$(document).ready(function() {
    var asyncTask = function(idx) {
        var dfd = $.Deferred(); // 获得一个 Deferred 实例
        setTimeout(function() {
            var result = parseInt(Math.random() * 100, 10) % 2,
                time = (new Date()).getTime(),
                prefix = "jQuery " + time + " " + idx + ":";
            if (result == 0) {
                dfd.resolve(prefix + "got an even number"); // 如果得到偶数,表示操作成功
            } else {
                dfd.reject(prefix + "failed to get an even number"); // 反之表示操作失败
            }
        }, 2000); // 延迟2秒执行函数
        return dfd; // 返回Deferred实例
    }
    
    function printResult (result) {
        console.info(result);
    }

    for (var i=0; i<5; i++) {
        asyncTask(i).then(printResult, printResult); // 执行5次,输出成功或者失败结果
    }
});
图 1.在 Chrome 运行的结果
在 Chrome 运行的结果
在 Chrome 运行的结果

需求实现

回到模拟场景,我们将创建一个 Employee 类,然后基于 Promise 规范创建三个关键方法:

  • 从服务端获取员工列表数据,代码见清单 2;
  • 从服务端获取详细数据;
  • 在客户端保存员工数据。

然后按场景需求,将这三个方法组合使用,代码见清单 3。

清单 2.从服务端获取员工列表数据的代码
Employee.all = function(opt) {
    var opt = opt || {local: true}, dfd = $.Deferred();
    if (opt.local === true) {
        throw new Error('not implemented!'); // 暂不实现离线存储部分的代码
    } else {
        // 通过ajax获取员工数据, jQuery的ajax方法也同样返回了一个Deferred实例
        $.ajax({
            url: "/async/api/employee",
            dataType: "json"
        }).then(function(items) {
            var employees = [];
            $(items).each(function(idx, item) {
                employees.push(new Employee(item))
            });
            dfd.resolve(employees);
        }, function(err) {
            dfd.reject(err);
        });
    }
    return dfd;
};
清单 3.实现模拟场景的代码
$(document).ready(function() {
    var msg;
    Employee.all({local: false}).then(function(employees) { // 从服务器读取员工数据
        $(employees).each(function(idx, employee) {
            if (employee.age >= 30) {
                Employee.get(employee.id, {local: false}). // 从服务器读取员工明细
                then(function(employee) {
                    employee.create({local: true}); // 将年龄大于30(含)的员工明细存于本地
                }, function(err) {
                    msg = 'failed to retrieve the detail info from the server';
                    console.error(msg);
                });
            } else {
                employee.create({local: true}); // 将年龄小于30的员工明细存于本地
            }
        });
    }, function(err) {
        msg = 'failed to retrieve all employee\'s info from the server.';
        console.error(msg);
    });
});
图 2.清单 3 代码的运行结果
清单 3 代码的运行结果
清单 3 代码的运行结果

由此可见,代码的可读性和可维护性得到了一定的提高,细心的读者也许会发现,for 循环中代码并不是顺序执行的。针对这个缺陷,我们会介绍两种非 Promise 的实现方式。

非 Promise 方式

Wind.js 介绍

Wind.js是 IBM 高级咨询师赵劼开源项目,引用他的话来说,是希望通过 JavaScript 这门语言体现 F# 中“计算表达式”特性,来达到简化异步编码的目的。基于 Wind.js 实现本文第一个例子,代码如下:

清单 4.基于 Wind.js 实现本文第一个例子的代码
var asyncTask = eval(Wind.compile("async", function (idx) { // 运行“编译”后的代码', 得到句柄
    for (var i=0; i<5; i++) {
        $await(Wind.Async.sleep(2000)); // 等待2秒再继续执行
        var result = parseInt(Math.random() * 100, 10) % 2,
            time = (new Date()).getTime(),
            prefix = "Wind.js " + time + " " + i + ":";
        if (result == 0) {
            console.info(prefix + "got an even number");
        } else {
            console.info(prefix + "got an odd number");
        }
    }
}));
asyncTask().start(); // 开始运行异步任务
图 3.清单 4 中代码的运行结果
清单 4 中代码的运行结果
清单 4 中代码的运行结果

由于使用了$await(Wind.Async.sleep(2000)) 这行代码,我们实现了每 2 秒获得一个奇数或者偶数。好奇的读者也许想知道 Wind.js 运行原理,简单的说,Wind.js 先对源代码进行词法分析、语法分析,然后动态生成新代码以实现异步编程。

需求实现

同样还是员工场景的那三个关键方法,我们将用到 Wind.js 的 Wind.Async.Task 类,将回调写法的代码封装起来,代码示例如下:

清单 5.从服务端获取员工列表数据的代码
Employee.all = function(opt) {
    var opt = opt || {local: true}, Task = Wind.Async.Task;
    if (opt.local === true) {
        throw new Error('not implemented!');
    } else {
        return Task.create(function(t) { // 创建 Wind.Async.Task实例, 封装必须使用回调方法的代码
            $.ajax({
                url: "/async/api/employee",
                dataType: "json"
            }).then(function(items) {
                var employees = [];
                $(items).each(function(idx, item) {
                    employees.push(new Employee(item)) 
                });
                t.complete("success", employees); // 操作成功
            }, function(err) {
                t.complete("failure", err); // 操作失败
            });
        });
    }
};
清单 6.实现模拟场景的代码
$(document).ready(function() {
    var employeeTask = eval(Wind.compile("async", function (idx) { // 生成句柄
        try {
            var employees = $await(Employee.all({local: false})); // 通过$await方法以便”编译”
            for (var i=0; i<employees.length; i++) {
                var employee = employees[i];
                if (employee.age >= 30) {
                    var employeeWithDetail = 
                        $await(Employee.get(employee.id, {local: false}));
                    $await(employeeWithDetail.create({local: true}));
                } else {
                    $await(employee.create({local: true}));
                }
            }
        } catch(err) {
            console.error(err);
        }
        
    }));
    employeeTask().start();
});

});

从清单 6 的代码不难看出,这种代码风格已经看不到 JavaScript 中常用的回调写法,毫无疑问,开发者会更容易接受这种“顺序执行”编程体验。

JSDeferred 介绍

JSDeferred则是一款独立、简洁的异步执行类库,通过 next、loop、call、parallel 和 wait 等方法实现异步编程。依然是基于 JSDeferred 改写本文第一个例子,代码如下:

清单 7.基于 JSDeferred 实现本文第一个例子的代码
Deferred.define(); // 将JSDeferred的方法变成全局方法
loop(5, function (i) { // 顺序循环5次
    var result = parseInt(Math.random() * 100, 10) % 2,
        time = (new Date()).getTime(),
        prefix = "JSDeferred " + time + " " + i + ":";
    if (result == 0) {
        console.info(prefix + "got an even number");
    } else {
        console.info(prefix + "got an odd number");
    }
    return wait(2); // 等待2秒
});
图 4.清单 11 中代码的运行结果
清单 11 中代码的运行结果
清单 11 中代码的运行结果

由于篇幅限制,下面只列出 JSDeferred 实现模拟场景的代码:

清单 8.实现模拟场景的代码
$(document).ready(function() {
    Deferred.define();

    next(function () {
        return Employee.all({local: false}); // 当从远端获得所有员工数据时,才运行下一步
    }).
    next(function (employees) {
        return loop(employees.length, function(i) { // 顺序遍历员工
            var employee = employees[i];
            if (employee.age >= 30) {
                return Employee.get(employee.id, {local: false})
                       .next(function(employeeWithDetail) {
                           return employeeWithDetail.create({local: true});
                       }); // 从远端获取员工明细,保存到本地
            } else {
                return employee.create({local: true});
            }
        })
    }).
    error(function(err) {
        console.error(err); // 异常会导致异步过程中断,并被打印
    })
});

由此可见,在改善异步编程体验方面,已经是百花齐放的局面,对于其他改善异步编程体验的方式,本文不一一罗列,感兴趣的读者可自行了解。

总结

浏览器之间的竞争以及移动终端持有量的增长加快了 HTML5 新特性的制定与实现,毫无疑问,这些新特性将会对代码的可读性和可维护性提出更高要求。希望读者阅读完本文后,对目前改善 JavaScript 领域的异步编程体验的方式有所了解,并能选择合适自己的方式。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=939218
ArticleTitle=浅谈如何降低 HTML5 的异步编程复杂度
publish-date=08012013