sojs

Simple Object-Oriented JavaScript

Usage no npm install needed!

<script type="module">
  import sojs from 'https://cdn.skypack.dev/sojs';
</script>

README

sojs - 大道至简

简单, 是一种美、一种哲学、一种信仰. sojs 提供了最佳的 javascript 编程方式, 让代码更加简单的编写、阅读和维护.

sojs 代码示例:

//创建cookie类
sojs.define({
    name: 'cookie',                 //类名
    namespace: 'sojs.utility',      //命名空间
    prefix: 'prefix-',              //属性
    getCookie:function(key){...}    //函数
});

//使用cookie类
var cookie = sojs.using('sojs.utility.cookie');
var id = cookie.getCookie('id');

sojs 是 编程框架 而非 类库 , 适用于所有的js项目, 可以和各种规范如ADM,CDM等一起使用. sojs中的so即Simple Object Oriented, 简单面向对象. 与传统的面向对象理论略有不同, 我们认为: 对象是组织代码的最小单位.

sojs 核心理念:

万物皆对象 对象皆JSON

sojs 主要功能:

使用JSON结构描述类 使用命名空间组织类 兼容全版本node环境 兼容全部浏览器环境


传统的JS编程方式

首先, 让我们了解为何传统js编程方式会导致代码的可读性下降. 因为js的灵活性, 在开发中经常会出现孤零的变量和函数, 比如:

var a = function(){return b};
var b = 'hello';
var c = a();
function d(){
    //...
}

所以我们常常见到这样组织代码的:

var property1 = 'a';
function method1(){...};
var property2 = 'b';
function method2(){...};
var property3 = '';
if(property1==='a'){
    property3 = 'c';
    var method3 = method1(property3);
}

再加上匿名函数和闭包的乱入:

//匿名函数和闭包举例
var property4 = 'd';
var method4 = method1(function(){
    function(){
        return property4;
    }
})

即使是有名的js项目或者大神们编写的代码已经尽量清晰的去组织变量和代码, 当代码量过多时也必然会导致可读性下降. 本质原因是传统的js开发 将变量作为了组织代码的最小单位 .

sojs的思想是 将对象作为组织代码的最小单位 . 实际上, 已经有很多的开发者意识到了这一问题.比如在最新版本的jQuery中源码, 已经通过使用JSON对象提高了代码可读性:

jQuery.extend({
    // Unique for each copy of jQuery on the page
    expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),

    // Assume jQuery is ready without the ready module
    isReady: true,

    error: function( msg ) {
        throw new Error( msg );
    },
    ...
    noop: function() {}
});

由此可见, 使用JSON字面量创建对象, 是最自然的面向对象编程方式.

基于此, sojs诞生了.


sojs入门

名词解释-全限定性名: 命名空间+类名.比如类C, 所在的命名空间是A.B, 则C的全限定性名是A.B.C

简单示例

下面是一个简单的node程序示例.

//node环境下引用sojs
require('node-sojs');

//定义cookie类
sojs.define({
    name: 'cookie',
    namespace: 'sojs.utility',
    getCookie:function(key){...},
    setCookie:function(key, value, option){...}
});

//使用cookie类, 因为cookie是静态类所以不需要实例化
var cookie = sojs.using('sojs.utility.cookie');

//使用cookie类的getCookie函数
var id = cookie.getCookie('id');

在sojs中, 使用JSON结构声明一个类. 通过sojs.define完成类的定义. 通过sojs.using获取到类的引用.

开始使用

下面详细的讲解使用sojs的步骤.

1. 引用sojs

sojs支持浏览器和node环境. 使用sojs的第一步就是引入sojs.

node 环境

使用npm或者cnpm安装最新版本的sojs. 在项目根目录运行npm命令:

npm install sojs

在程序的入口处引用:

require('sojs');

整个程序进程只需要引用一次.

浏览器环境

在浏览器环境下, 需要手动下载sojs文件. 项目地址:

https://github.com/zhangziqiu/sojs

sojs项目的bin目录下面, 有两个js文件:

  • sojs.core.js: 仅包括核心的面向对象编程功能.
  • sojs.js: 除了核心功能, 还包括loader类用于加载依赖类. event类用于处理事件编程. promise类用于异步编程. 如果对大小不是特别敏感, 通常都加载此文件.

将sojs.js下载到项目中后, 直接引用即可:

<script type="text/javascript" src="./bin/sojs.js"></script>

2. 创建类

[项目根目录]/src/utility 目录下创建template.js文件, 内容如下:

    //template类提供一个render方法用于模板和数据的拼接
    sojs.define({
        name: 'template', // node环境可忽略, name取值为文件名
        namespace: 'utility', // node环境可忽略, namespace取值为相对于src的文件夹路径
        render: function (source, data) {
            var regexp = /{(.*?)}/g;
            return source.replace(regexp, function (match, subMatch, index, s) {
                return data[subMatch] || "";
            });
        }
    });

上面就是一个完整类的代码. 在sojs中, js类文件都是以sojs.define开始, 里面包括一个JSON格式的对象. 开发者可以自由的添加属性和方法, 但是要注意 此JSON对象的以下属性是sojs框架使用的:

  • name: 类名, 比如上面的 template. 在node环境中, 如果不设置name, 则自动以文件名作为类名.
  • namespace: 类所在的命名空间. 比如template类放在了 utility 命名空间下. 我们可以将所有的工具类都放在utility命名空间下,实现类的组织管理. 在node环境中, 如果不设置namespace, 则自动以文件夹路径作为命名空间.
  • 名字为 "类名" 的函数(比如template类的template函数): 类的构造函数. 使用 sojs.create 创建一个类实例时, 会执行一次构造函数.
  • 名字为 "$+类名" 的函数(比如template类的$template函数): 类的静态构造函数. 当使用sojs.define将一个类引入时, 会执行一次静态构造函数. 创建实例的时候不会执行. 多次执行sojs.define只有第一次引入时会调用一次.
  • deps: 依赖的类. 使用object描述. key为引用变量名, value为类的全限定性名. 后续可以通过this.key引用到这个依赖类. 在构造函数和静态构造函数执行时,所有的deps依赖类都已经加载完毕.可以安全的使用依赖类. 在构造函数或者静态构造函数中可以放心的使用this.key的方式使用依赖类. 有关加载依赖的详细说明后面会有单独的章节讲解.

3. 使用类

现在我们已经有了一个template类. 下面介绍如何在不同的环境下使用template类.

3.1 创建入口类

通常程序都有一个main函数作为程序入口, 在sojs中稍有不同, 借助sojs的依赖管理和静态构造函数, 我们可以构造一个入口类main.js:

    sojs.define({
        name: 'main',
        deps: { template: 'utility.template' },
        $main: function(){
            var result = this.template.render('My name is {name}', {name:'ZZQ'});
            console.log(result);
        }
    });

main.js可以放置在任意目录,而且也没有命名空间. main的静态构造函数$main作为程序的入口, 在静态构造函数中通过"this.template"引用到template类.

3.2 node环境运行

在node环境中运行main.js, 需要在main.js顶部添加引用sojs库的代码:

    //引用sojs库
    require('sojs');
    //后面就是main类的完整代码.
    sojs.define({
        name: 'main',
        deps: { template: 'utility.template' },
        $main: function(){
            var result = this.template.render('My name is {name}', {name:'ZZQ'});
            console.log(result);
        }
    });

运行:

    node main.js

即可看到输出结果:

    My name is ZZQ

在node环境下, 当加载 utility.template 类时, 默认会从如下路径加载类文件:

[node运行目录]/src/utility/template.js

即将 [node运行根目录]/src 目录作为代码存放的根目录, 每一个命名空间就是一个文件夹.

3.3 浏览器环境

假设项目目录就是网站的根目录, 并且网站名称是 localhost. 在根目录下创建main.html, 编写如下代码:

<!DOCTYPE html>
<html>
    <body>
        <!-- 引入sojs, 假设将sojs.js下载到了src目录中 -->
        <script type="text/javascript" src="./src/sojs.js"></script>
        <!-- 设置类文件根目录 -->
        <script>
            sojs.setPath('http://localhost/src/');
        </script>
        <script>
                //下面是main.js的内容, 可以将main直接写在页面里
                sojs.define({
                    name: 'main',
                    deps: { template: 'utility.template' },
                    $main: function(){
                        var result = this.template.render('My name is {name}', {name:'ZZQ'});
                        console.log(result);
                    }
                });
        </script>
    </body>
</html>

打开 http://localhost/main.html页面, 即可在console控制台中看到:

    My name is ZZQ

在浏览器中使用时, 需要设置类文件的根目录.sojs框架将从指定的根目录, 使用异步的方式加载类. 比如上面的例子加载 utility.template 类的地址是:

    http://localhost/src/utility/template.js

如果main有多个依赖类, 会同时并行异步加载, 并且在所有的类都加载完毕后在运行$main静态构造函数.

通过上面实例的详细讲解, 已经可以使用sojs编写简单可维护的js代码了. 下面的篇幅会介绍一些sojs细节和高级用法.


类的加载路径

使用sojs的项目代码是用类组织的.在入门示例中, template.js 加载时, 默认使用如下路径:

node: [node运行的目录]/src/utility/template.js

浏览器: [页面当前域名]/src/utility/template.js

即无论是node还是浏览器, 在项目的根目录里创建src文件夹, src内根据命名空间来组织文件夹.

sojs提供了setPath函数, 可以为特定的类或者命名空间指定自己特殊的加载路径.

//设置全局根目录
sojs.setPath('./src2/');
sojs.setPath('http://localhost/asset/js/');

//为 A 和 A.B 命名空间设置特殊的加载路径
sojs.setPath({
    'A':'./src1/A/',
    'A.B':'./src2/A/B/'
});

sojs的路径查找使用的是树状结构. 以"A.B.C"这个类的查找规则为例:

  • 首先从root根目录查找. 此时path为默认的src目录.
  • 查找命名空间A, 发现通过setPath设置了A的路径, 此时path为上面设置的 './src1/A'
  • 查找命名空间A.B, 发现通过setPath设置了A.B的路径, 此时path为上面设置的 './src2/A/B/'
  • 查找命名空间A.B.C, 未发现A.B.C的路径, 于是使用上一步的path作为结果返回

通过上面的查找行为可以看出, 优先使用更加精确的类的加载路径, 否则使用父路径.


sojs的git源码结构

bin目录

bin目录中, 用于存放编译后的文件. bin目录下有以下两个js文件:

  • sojs.core.js: 压缩后的文件. 仅包括核心的面向对象编程功能.
  • sojs.js: 压缩后的文件. 除了核心功能, 还包括loader类用于加载依赖类.以及event类用于处理事件编程. 通常都加载此文件.

在bin的根目录下, 放置的是代码压缩后的js文件. 通常在项目中引用的就是代码压缩后的js文件. 同时为了应对一些比如调试等场景, bin目录下还包括如下文件夹:

  • format文件夹: 格式化,但是无注释的编译结果
  • gzip文件夹: gzip后的编译结果
  • source文件夹: 包含注释的编译结果

上面每一个文件夹中, 也都包含 sojs.core.js和sojs.js两个文件(gzip目录下是.gz后缀), 可以根据需要使用.

src目录

src目录存放sojs的源码文件. sojs框架自身也是按照sojs的编程方式实现的, 即sojs是自解析的.

  • sojs文件夹 : sojs命名空间
    • event.js : sojs.event类实现了事件编程和promise编程模式. 这是异步编程时必不可少的类.
    • loader.js : sojs.loader类提供了浏览器环境加载依赖类的加载器.
  • sojs.js : sojs的核心编程框架. sojs.core.js就是直接从这个文件编译出来的.

项目根目录

bin和src两个目录是最主要的文件夹. 除了这两个文件夹, sojs的项目根目录还包括如下文件:

  • .gitignore: git配置文件, 里面写明了git不需要管理的文件. 比如 node_modules 文件夹.

  • README.md: 说明文档. 即您正在阅读的这个页面的内容.

  • make.js: 编译文件. 使用 node make.js 命令, 可以从src目录生成bin目录下面的所有文件.

  • package.json: 包描述文件.


事件函数与this指针

假设我们声明了一个类:

    sojs.define({
        name:'myClass',
        word: 'Hello World',
        say: function(){
            alert(this.word);
        }
    })

myClss类有一个say函数, 输出myClass类的word属性. say函数中通过this引用myClass类自身. 这在通常情况下都是正确的.

但是在事件中, 比如常见的按钮单击事件, 或者一个定时器函数, this指针并不是总指向myClass自身的:

    window.word = 'I am window';
    var myClass = sojs.using('myClass');
    setTimeout(myClass.say, 1000);

上面的代码会输出"I am window"而不是myClass类中的"Hello World". 因为在setTimeout中的this指向了window对象而不是myClass.

sojs提供了proxy函数用于解决this指针问题. 默认情况下为了使用方便, 会为function的原型添加proxy函数. 如果不希望对原型造成污染也可以通过配置取消此功能.

proxy函数用来修改事件的this指针. 比如上面的代码可以这样修改:

    var myClass = sojs.using('myClass');
    setTimeout(myClass.say.proxy(myClass), 1000);

使用了proxy之后, 可以正常的输出"Hello World".

proxy函数的第一个参数表示this指针需要指向的对象.

proxy函数还可以 修改事件处理函数的签名 , 下面举一个复杂的例子.

在nodejs中, 系统提供了socket对象, 用于网络编程.

var net = require('net');
var client = net.connect({port: 8124},
    function() { //'connect' listener
          console.log('client connected');
          client.write('world!\r\n');
});

调用 net.connect函数时, 需要传递一个回调函数, 并且回调函数是无参数的. 通常, 使用上面的例子, 我们传递了一个匿名的回调函数, 并且在这个回调函数中使用 client变量, 此时会生成一个闭包, 以便在回调函数执行时, 可以正确访问到client变量.

使用proxy函数, 可以用一种 显式闭包 的方式, 将client作为参数传递, 让其看起来是通过参数传递的而不是使用闭包:

var net = require('net');
var client = net.connect({port: 8124},
    function(mySocket) { //'connect' listener
          console.log('client connected');
          mySocket.write('world!\r\n');
}.proxy(this, client));

注意, 这里通过proxy除了传递this对象外, 还传递了client变量. connect原本的回调函数是没有签名的, 但是你会发现在回调函数执行时, mySocket可以被正常访问. 此时我们将原本无参数的事件处理函数, 变成了一个有参数的事件处理函数.

proxy函数看似神奇,其实内部还是使用闭包实现. 所以我在这里称其为 显式闭包 . 使用显示闭包极大的增加了代码的可读性和可维护性. 可以说显示闭包让邪恶的闭包从良了.

另外, 显示闭包还可以解决循环中使用闭包的常见错误, 看下面的例子: (参见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures)

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus 事件处理函数,以便显示帮助信息。

运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。

该问题的原因在于赋给 onfocus 是闭包(showHelp)中的匿名函数而不是闭包对象;在闭包(showHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus 的回调被执行时,循环早已经完成,且此时 item 变量(由所有三个闭包所共享)已经指向了 helpText 列表中的最后一项。

使用proxy函数就不会出现上面的问题:

    document.getElementById(item.id).onfocus = function(ev, item) {
      showHelp(item.help);
    }.proxy(this, item);

特别要注意, proxy函数只能在原有的事件处理函数后面新增参数. onfocus事件原本是包括一个事件对象参数的. 即上面的ev. 所以需要将item作为第二个函数参数使用.

相对于传统的node变成, 下面来看看使用sojs实现的完整的socket服务器的例子:

require('node-sojs');


sojs.define({
    name: 'socketServer',    
    /**
     * 静态构造函数
     */
    $socketServer: function () {
        var net = require('net');
        
        //启动服务
        this.server = net.createServer();
        this.server.on('connection', this.onConnection.proxy(this));
        this.server.on('error', this.onError.proxy(this));
        this.server.listen(8088, function () {
            base.log.info('server bound');
        });
    },

    /**
     * 服务器连接事件处理函数. 
     * @param {Object} socket 本次连接的socket对象
     */
    onConnection: function (socket) {
        socket.on('data', this.onData.proxy(this, socket));
        socket.on('end', this.onEnd.proxy(this, socket));
        socket.on('error', this.onError.proxy(this, socket));
    },

     /**
     * socket接收数据事件处理函数. 
     * @param {Object} data 本次介绍到的数据对象buffer
     * @param {Object} socket 本次连接的socket对象
     */
    onData: function (data, socket) {
        //do something...
    },

    /**
     * socket 关闭事件处理函数. 
     * @param {Object} socket 本次连接的socket对象
     */
    onEnd: function (socket) {
        //do something...
    },

    /**
     * socket 异常事件处理函数. 
     * @param {Object} err 异常对象
     * @param {Object} socket 本次连接的socket对象
     */
    onError: function (err, socket) {
        //do something...
    }
});

sojs的原型继承和快速克隆

sojs中使用特有的快速克隆方法实现高效的对象创建. 主要用在内部的sojs.create函数中, 此函数用于创建一个类实例.

假设a是classA的一个实例. 此时的原型链情况如下:

a.contructor.prototype->classA

当访问a的一个属性时, 有以下几种情况:

  1. 属性是值类型: 访问: 通过原型链获取到classA的属性 赋值: 在a对象上设置新的属性值, 再次访问时获取到的是a对象上的新值
  2. 属性是引用类型(比如object类型): 访问: 通过原型链获取到classA的属性 赋值: 因为是引用类型, 所以实际上是对classA上的引用对象赋值. 即classA被修改, 所有实例的此属性都被修改

为了解决此问题, sojs在创建classA的实例时, 会遍历classA的属性, 如果发现属性的类型是引用类型, 则对其进行快速克隆:

        /**
         * 快速克隆方法
         * @public
         * @method fastClone
         * @param {Object} source 带克隆的对象. 使用此方法克隆出来的对象, 如果source对象被修改, 则所有克隆对象也会被修改
         * @return {Object} 克隆出来的对象.
         */
        fastClone: function (source) {
            var temp = function () {};
            temp.prototype = source;
            var result = new temp();
        }

传统的克隆对象是十分消耗性能的, sojs的最初也是用了传统的克隆方法. 最后改进成使用快速克隆方法.

假设这个属性为A, 此时相当于将属性A作为类, 创建了一个属性A的实例, 即关系是:

a.A.constructor.prototype -> classA.A 

此时, 如果A的所有属性都不是引用类型, 则可以解决上面的赋值问题. 但是如果属性A本身, 又含有引用类型, 则同样会出现赋值是修改原形的问题. 假设: A.B为object 则通过 a.A.B 获取到的对象与 classA.A.B 获取到的对象是同一个对象. 对于a.A.B的修改同样会影响到classA.A.B 通过递归的快速克隆可以解决此问题, 但是因为性能开销太大, 所以sojs最后不支持多层的对象嵌套.

实际上, 我们可以通过编程方式来解决这个问题.

  • 在类声明时赋值的属性, 即JSON中直接描述的属性值, 应该是静态static类型. 不应在运行时修改.
  • 如果一个属性是实例属性, 则应该在动态构造函数中赋值.比如:
sojs.define({
    name: 'classA'
    A: null
    classA:function(){
        this.A = { B:1 }
    }
});

所以一定要注意, 如果类的属性是对象, 并且是实例属性(运行时会被修改),则必须在动态构造函数中创建.

另改一个问题就是a对象的遍历. 同样因为使用了原型继承, 不能够通过hasOwnProperty来判断一个属性是否是实例a的. 可以通过遍历classA来解决:

for(var key in a){
    if(key && typeof a[key] !== 'undefined' && classA.hasOwnProperty(key)){
        //do something...
    }
}

事件编程

js中常常使用事件和异步, 在浏览器端的Ajax是异步, 在nodejs中更是到处都是异步事件.

在异步事件的编程中, 常常会遇到多层嵌套的情况. 比如:

var render = function (template, data, l10n) {
  //do something...
};

$.get("template", function (template) {
  // something
  $.get("data", function (data) {
    // something
    $.get("l10n", function (l10n) {
      // something
      render(template, data, l10n);
    });
  });
});

在异步的世界里, 需要在回调函数中获取调用结果, 然后再进行接下来的处理流程, 所以导致了回调函数的多层嵌套, 并且只能串行处理.

sojs提供了sojs.event, 来解决此问题. 比如要实现上面的功能, 可以进行如下改造:

var render = function (template, data, l10n) {
  //do something...
};

var ev = sojs.create(sojs.event);
ev.bind('l10n', function(data){
    ev.emit('l10n', data);
});
ev.bind('data', function(data){
    ev.emit('data', data);
});
ev.bind('template', function(data){
    ev.emit('template', data);
});
//并行执行template, data和l10n事件, 都执行完毕后会触发group中的回调函数
ev.group('myGroup', ['template','data','l10n'], function(data){
    render(data.template, data.data, data.l10n);
});

sojs.event的group可以将事件打包成一组. 在group的回调函数中, 会传递一个参数data, 这是一个object对象, 其中key为group中绑定的每一个事件名, value为事件的返回值. 所以可以通过data[事件名]获取到某一个事件的返回值.

sojs.event中的group还可以动态添加新的事件. 比如:

ev.group('myGroup', ['template','data','l10n'], function(data){
    render(data.template, data.data, data.l10n);
});

ev.group('myGroup', ['another'], function(data){
    anotherData = data.another;
});

注意上面的代码, 虽然为myGroup又添加了一个another事件. 但是此时mygroup绑定了两个事件处理函数, 这两个函数都会在所有事件完成时执行, 但是不一定哪个在前. 所以sojs.event还提供了afterGroup事件, 此事件会在所有group绑定的callback执行完毕后再执行:

ev.group('myGroup', ['template','data','l10n']);

ev.group('myGroup', ['another']);

ev.afterGroup('myGroup', function(data){
    render(data.template, data.data, data.l10n, data.another);
});

sojs.event使用oo的思想实现. node中本身自带EventEmmiter也实现了部分功能.


Promise 编程

使用event事件编程, 可以解决回调函数嵌套的问题. 但是是否有更好的解决办法呢? 答案是使用Promise.

通过举例, 来快速的了解什么是Promise.

传统的回调函数方式编程, 在这个例子中, step1,step2,step3 三个函数都是异步顺序执行的:

var step1,step2,step3 = function(data, callback){
    var result = data + 1;
    callback(result);
}

// step1开始执行
var data1 = 1;
step1(data1, function(data2){
    // step2开始执行
    step2(data2, function(data3){
        // step3开始执行
        step3(data3, function(data4){
            // 输出最终执行结果
            console.log(data4);
        })
    })
})

使用event事件编程, 绑定了一个事件序列:

var ev = sojs.create(sojs.event);
ev.bind('step1', function(data1){
    // step1开始执行
    var data2 = data1 + 1;
    ev.emit('step2', data2);
});
ev.bind('step2', function(data2){
    // step2开始执行
    var data3 = data2 + 1;
    ev.emit('step3', data3);
});
ev.bind('step3', function(data3){
    // step3开始执行
    var data4 = data3 + 1;
    // 输出最终执行结果
    console.log(data4);
});

// 从step1开始
ev.emit('step1', 1);

使用Promise编程, 使用then函数将事件串联起来:

// 创建了一个完成态的promise对象,下一个then会立刻执行,并且传递参数 1.
var promise = sojs.promise.resolve(1);
promise.then(function(data1){
     // step1开始执行
     var data2 = data1 + 1;
     return data2;
}).then(function(data2){
     // step2开始执行
     var data3 = data2 + 1;
     return data3;
}).then(function(data3){
     // step3开始执行
     var data4 = data3 + 1;
     // 输出最终执行结果
    console.log(data4);
})

从上面三个实例可见, event和promise都可以解决回调函数嵌套的问题.

不同的是, event需要创建三个事件, 以step1事件为例,需要在step1的事件处理函数中,显示的调用step2.即事件序列的执行迅 速分散在了每一个事件处理函数中.

而使用promise时, 是由一个promise对象通过调用then函数, 将step1-3串联起来, 整个事件的执行顺序集中在了一起管理.

有关promise的深入学习,推荐以下学习资料: 《JavaScript Promise迷你书(中文版)》 http://liubin.org/promises-book/#ch2-promise-all


为什么要用面向对象的思想写js?

oo不仅仅是一种编程方法, 而是组织代码的最小单位.

看几个使用AMD规范的例子就会明白, AMD中最后一个参数factory虽然美其名曰构造函数, 但是在这个函数中, 你可以做任何事情:创建局部function, function中再嵌套function, 使用闭包, 处理一些业务逻辑. 最后的结果是这个factory不易阅读和维护.

究其原因, js编程很容易陷入面向过程编程的方式中. 而AMD等规范只注重"模块"的开发, 却忽视了一个模块内部的代码如何组织和管理.

js中让代码不易管理的几个杀手包括: 闭包, 零散的函数对象, 异步机制(node中尤其重要).

sojs使用oo的思想, 减少闭包的使用, 让每一个函数对象都挂靠在类对象上, 减少孤零的函数对象的存在. 再配合sojs.event的事件机制, 解决异步编程中的事件嵌套噩梦.

可以说sojs为js的大规模开发提供了有效地基础保障.


加入我们

sojs还在发展中, 我们尽量不在核心的sojs.js中加入过多的功能, 保持核心精简. 同时通过sojs团队成员的努力, 让sojs适用于更多的场景.

欢迎有志之士加入到sojs的开发中来!