Name: Password: Sign in
LiKy - 一个 Javascript MVVM 框架的实现 中的文章

本文在 署名-非商业性使用-相同方式共享 3.0 版权协议下发布, 转载请注明出自 kyleslight.net

(伪)模型

首先我们将考虑的是 MV* 中连接 M 与 V 的部分。

想象一下牵线木偶中的人的手指。人的手指非常灵活,因而我们把重心专注于对手指(模型)的操作上。对于模型本身我们可以实现 CRUD 等操作,这些改变可以通过线(控制器)将变化反映到木偶(通常是较为复杂的视图)中,这样一种对数据与视图分离,用简单与本质的模型的变化间接作用于复杂视图的做法在各种程序设计中被证明是非常有效的。

沿着上一篇的思路,我们这一篇来创建模型类。

伪模型类

我们已经实现了类与继承,因而可以很容易地创建对象。但这距离我们实现利用 M 来驱动 V 的目标仍然很远。首先我们需要划定空间,即需要知道哪些对象是被用来作为模型使用的。因此我们引入伪模型类,就像下面这样:

var Model = new Class;

Model.extend({
    createInstance: function () {
        var i = Object.create(this.prototype);
        i.parent = this;
        i.init.apply(i, arguments);
        i.create.apply(i, arguments);
        return i;
    },
    createClass: function (attrs) {
        var o = Object.create(this);
        o.parent = this;
        o.fn = o.prototype = Object.create(this.prototype);
        o.include(attrs);
        this.created();
        this.inherited(o);
        return o;
    },
    // callbacks
    inherited : function () {},
    created: function () {}
});

Model.include({
    init: function () {}
});

如同上篇所说的那样,ModelClass 模板创建,因而具有 extendinclude 等方法。

Model 是一个抽象的模型,我们需要得到的真实模型继承自它。它主要有两个方法,一个是保证原型链传递下去的 createClass,它用来创建模型类;一个是创建实例的 createInstance,用下面的例子来简单介绍它的使用:

var Post = Model.createClass({
    _title: 'no title',
    _content: 'no content',
    init: function (title, content) {
        this._title = title;
        this._content = content;
    },
    printSelf: function () {
        console.log('{ Post title: %s content: %s }', this._title, this._content);
    }
});
var post = Post.createInstance('This is title', 'This is content');
post.printSelf(); // { Post title: This is title content: This is content }

var PostInOldBrowser = Post.createClass({
    printSelf: function () {
        console.log('{ I\' a post, but I\' in an old browser +_+, title: %s content: %s }', this._title, this._content);
    }
});
var postInOldBrowser = PostInOldBrowser.createInstance('This is another title', 'This is another content');
postInOldBrowser.printSelf(); // { I' a post, but I' in an old browser +_+, title: This is another title content: This is another content }

记录

上面的模型类虽然帮助我们完成了模型的建立,但似乎与普通的对象创建没什么区别。我们想要模型变化时自动得到某种提示至少需要以下两步:

  • 模型类本身知道它实例的存在
  • 实例的变化会通知到模型类

上面的第一步需要额外的空间来保存对实例的引用,因而我们这一节来创建这种引用,也就是记录。

模型类中的记录

Model.extend({
    ...
    createClass: function (attrs) {
        var o = Object.create(this);
        o._records = {};
        ...
    },
});

记录的结构创建完了,就这么简单:)

GUID

接下来一步我们需要给每个记录分配一个 id,当然你可以在创建记录的时候每一条都通过某些属性来编一个 id ,但是我们更喜欢让系统自动分配,这样之后就不用在乎创建 id 这件事。

有很多种自动生成 id 的方法,GUID 是其中的一个例子:

var GUID = function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0;
        var v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
}

这个函数会随机生成一个 32 位( 128 Bit )的 16 进制数,想深入了解算法的来由可以参考 Wikipedia 的介绍

创建记录

Model.extend({
    ...
    onRecordCreated: function () {}
});

Model.include({
    ...
    create: function () {
        if(!this._id) this._id = GUID();
        this.parent._records[this._id] = this;
        this.onCreated(this);
        this.parent.onRecordCreated(this);
    }
});

createInstance 会在每一个对象初始化后给它创建一个 id 添加进记录,于是我们可以由此来管理所有模型类创建的对象:

var Student = Model.createClass({
    _name: 'No name',
    init: function (name) {
        this._name = name;
    },
    getSelfInfo: function () {
        return '{ Student name: ' + this._name + ' id: ' + this._id + ' } ';
    }
});

Student.extend({
    printSelf: function () {
        console.log('{ Students records: %s }', this.getPlainRocord());
    },
    getPlainRocord: function () {
        var text = '';
        for(var key in this._records)
            text += this._records[key].getSelfInfo();
        return text;
    }
});

var Andy = Student.createInstance('Andy');
Student.printSelf(); // { Students records: { Student name: Andy id: 64d0d6a2-ea28-45b7-905f-7fb5a7d730a9 }  }

var Beth = Student.createInstance('Beth');
Student.printSelf(); // { Students records: { Student name: Andy id: 64d0d6a2-ea28-45b7-905f-7fb5a7d730a9 } { Student name: Beth id: 831b6d4c-4f0f-4ce7-b5ce-3dede7e90b17 }  }

实例状态

「实例的变化会通知到模型类」就是我们要实现的第二步。

Objectobserve 方法可以做到监听对象状态变化的事件并传入回调函数异步进行处理。不过这个方法浏览器支持率不高,也并不被推荐使用( 参考 http://caniuse.com/#search=Object.observe )。

实际上我们可以把思路改成:只有当我主动做出了某些对实例的改变才会通知到模型类。

实例状态的初始化

出于最小化状态的需要以及避免监听开销的考虑,我们希望模型的状态只由实例的属性 state 所保有,而实例的方法只在 createClass 时被初始化,为此我们做了以下变化:

Model.extend({
    ...
    createInstance: function () {
        var i = Object.create(this.prototype);
        i.parent = this;
        // i.init.apply(i, arguments);
        i.create.apply(i);
        i.initialState.apply(i, arguments);
        return i;
    },
    ...
});

Model.include({
    ...
    _state: {},
    ...
    initialState: function (s) {
        if (typeof s !== 'object') {
            console.warn('Parameter of createInstance should be an object.');
            return;
        }
        this.deepCopy(s, this._state);
        this.onStateCreated(this);
    },
    getState: function (attr) {
        if (!this._state[attr]) {
            console.warn('State attribute %s is not available.', attr);
            return null;
        }
        return this._state[attr];
    },
    deepCopy : function (resource, target) {
        if(typeof resource !== 'object') return resource;
        for (var i in resource) {
            target[i] = this.deepCopy(resource[i], target);
        }
        return target;
    },
    // callbacks
    onCreated: function () {},
    onStateCreated: function () {}
});

这样,创建实例时我们只接受一个 object ,进行深拷贝后用它来初始化我们的 state,同时暴露获取状态的接口 getState

var Movie = Model.createClass({
    printSelf: function () {
        console.log('{ Movie title: %s, price: $%f }', this.getState('title'), this.getState('price'));
    }
});

var Zootopia = Movie.createInstance({
    title: 'Zootopia',
    price: 10
});
Zootopia.printSelf(); // '{ Movie title: Zootopia, price: $10 }'

实例状态的变化

接下来我们来主动设置实例状态:

Model.extend({
    ...
    addClassListener: function () {this.extend.apply(this, arguments)},
    ...
    onRecordChanged: function () {}
});

Model.include({
    ...
    setState: function (stateDiff) {
        if (typeof stateDiff !== 'object') {
            console.error('Parameter of setState should be an object');
            return;
        }

        var oldState = {};

        for (var attr in stateDiff)
            oldState[attr] = this._state[attr];
            this._state[attr] = stateDiff[attr];

        // provide old state, wrap getState method to make it act like an old instance of Model
        oldState.getState = function (attr) {
            return oldState[attr];
        }

        if (stateDiff) this.parent.onRecordChanged(this, oldState);
        this.onStateChanged(this, oldState);
    },
    ...
});

这样 state 会被覆写并通知模型类来对变化进行处理。

var Movie = Model.createClass({
    printSelf: function () {
        console.log('{ Movie title: %s, price: $%f }', this.getState('title'), this.getState('price'));
    },
    priceOff: function (percentage) {
        this.setState({
            price: this.getState('price') * percentage
        });
    }
});

Movie.addClassListener({
    recordChanged: function (newIns, oldIns) {
        console.log('I heard the price of %s got changed from $%f to $%f }', 
            newIns.getState('title'), oldIns.getState('price'), newIns.getState('price'));
    }
});

var Zootopia = Movie.createInstance({
    title: 'Zootopia',
    price: 10
});
Zootopia.printSelf(); // '{ Movie title: Zootopia, price: $10 }'
// somehow the ticket is on sale
Zootopia.priceOff(0.8); // 'I heard the price of Zootopia got changed from $10 to $8'
Zootopia.printSelf(); // '{ Movie title: Zootopia, price: $8 }'

删除实例

我们已经实现了 CRUD 中的前三个,还剩下实例以及它内部状态的消亡,这件事就非常简单了:

Model.extend({
    ...
    onRecordDestroyed: function () {}
});

Model.include({
    ...
    destroy: function () {
        this.onStateDestory(this);
        this.onDestory(this);
        this.parent.onRecordDestory(this);

        delete this.parent._records[this._id];
    },
    ...
    onStateDestroyed: function () {},
    onDestroyed: function () {}
});

简单示例一下:

var Teletubbies = Model.createClass({
    onDestroyed: function () {
        console.log('天线宝宝要说再见啦天线宝宝要说再见啦');
    }
});

Teletubbies.addClassListener({
    onRecordDestroyed: function (record) {
        console.log('哦我的天线宝宝 %s 说再见了', record.getState('name'));
    }
});

var Tinky = Teletubbies.createInstance({
    name: 'Tinky'
});
Tinky.destroy(); // '天线宝宝要说再见啦天线宝宝要说再见啦'  '哦我的天线宝宝 Tinky 说再见了'

实例的生命周期

如果你足够细心的话应该会发现我们已经准备好了实例到达生命周期各个阶段触发的回调函数,包括:

实例本身的创建与销毁:

  • onCreated
  • onDestroyed

实例状态创建,改变与销毁:

  • onStateCreated
  • onStateChanged
  • onStateDestroyed

模型类接收到记录的创建,改变与销毁:

  • onRecordCreated
  • onRecordChanged
  • onRecordDestroyed

其中只有 onStateChangedonRecordChanged 这两个涉及到状态变化的触发会传递包含新实例与旧实例( 旧实例实际上是旧状态的子集包装上 getState 方法的伪实例 )的引用,其他均只传递实例本身的引用。

添加模型类的静态方法( 同样可以添加回调函数的监听器 ):

Model.extend({
    ...
    addClassStatic: function (staticMethods) {
        for (var method in staticMethods) {
            this[method] = staticMethods[method];
        }
    },
    ...
});

整个模型类的实现差不多结束了,下面我们用一个全面的例子来展示生命周期的表现。这个例子是一个纯基于 console 的游戏,设定如下:

  • 游戏分为 Allies(你)、Enemy 两方,一开始分别创建若干士兵
  • 每个士兵都随机分配一个初始的 damage (攻击值)、defence (防御值)、health(生命值)与 pronoun (第三人称代词),同时按顺序编号
  • 每一回合双方随机选择一名士兵战斗,互相进行攻击 attack,被攻击的一方受到伤害 getInjured,如果攻击方的 damage 超过防御方的 defence,防御方的生命值将随之减少
  • 生命值减少到特定值之下的士兵攻击时会有 bonus,生命值小于 0 的士兵死亡并退出战场
  • 任何一方士兵减少到 0 游戏结束,另一方获胜

游戏过程中士兵的诞生,受伤以及死亡都会触发事件,测试所有的事件监听会导致日志过多(意义也不大),所以我们选取了其中的一个子集(模型类接收记录产生事件的回调)onRecordCreatedonRecordChangedonRecordDestroyed 来看看效果如何。

var Soldier = Model.createClass({
    LOWHEALTH: 100,
    EXTREMELOWHEALTH: 20,
    getInjured: function (value) {
        var overDamage = value - this.getState('defence');

        this.setState({
            'health': (overDamage < 0 ? this.getState('health') : this.getState('health') - overDamage)
        });
        if (this.getState('health') < 0) 
            this.destroy();
    },
    attack: function (opposite) {
        var health = this.getState('health');
        var damage = this.getState('damage');

        if (health < this.EXTREMELOWHEALTH) damage *= 2;
        else if (health < this.LOWHEALTH) damage *= 1.5;

        opposite.getInjured(damage);
    }
});

Soldier.addClassStatic({
    onCreated: function () {
        this.SOLDIERNUM =  2;
        this.MAXDAMAGE = 200;
        this.MAXDEFENCE = 50;
        this.MAXHEALTH = 500;
        this.soldierNum = 0;
    },
    generateSoldiers: function () {
        for (var i = 0; i < this.SOLDIERNUM; i++) {
            this.createInstance({
                damage: this.reasonableRandomValue(this.MAXDAMAGE),
                defence: this.reasonableRandomValue(this.MAXDEFENCE),
                health: this.reasonableRandomValue(this.MAXHEALTH),
                pronoun: (Math.random() < 0.8 ? 'his' : 'her'),
                No: i + 1
            });
        };
    },
    chooseOne: function () {
        var keys = Object.keys(this._records);
        return this.getRecord(keys[ keys.length * Math.random() << 0]);
    },
    reasonableRandomValue: function (origin) {
        return Math.floor(0.5 * (1 + Math.random()) * origin);
    }
});


var Allies = Soldier.createClass({});
var Enemy = Soldier.createClass({});

Allies.addClassStatic({
    onRecordCreated: function (allies) {
        this.soldierNum++;
        console.log('A soldier has been created, and now we have %d soldier(s)', this.soldierNum);
        console.log('- Soldier: 来不及解释了快上车!');
    },
    onRecordChanged: function (allies) {
        console.log('Soldier No.%f gets injured, now %s health is %f', allies.getState('No'), allies.getState('pronoun'), allies.getState('health'));
        console.log('- Soldier: 破机车发不动... ');
    },
    onRecordDestroyed: function (allies) {
        this.soldierNum--;
        console.log('Soldier No.%f has been dead, now we have %d soldier(s)', allies.getState('No'), this.soldierNum);
        console.log('- Soldier: 车...开走了吗');
    }
});

Enemy.addClassStatic({
    onRecordCreated: function () {
        this.soldierNum++;
        console.log('- Enemy : Ao!!!!!!!');
    },
    onRecordChanged: function () {
        console.log('- Enemy : Ao!!!!!!!!!!!!!!!!!')
    },
    onRecordDestroyed: function () {
        this.soldierNum--;
        console.log('- Enemy : Ao!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
    }
});

Allies.generateSoldiers();
Enemy.generateSoldiers();

while (Allies.soldierNum && Enemy.soldierNum) {
    var allies = Allies.chooseOne();
    var enemy = Enemy.chooseOne();
    allies.attack(enemy);
    if (Enemy.soldierNum)
        enemy.attack(allies);
}

var sign = (Allies.soldierNum > 0 ? 'You win!' : 'You lose.');

console.log("=====result=====\n\n\t%s\n\n================", sign);

以下是日志:

A soldier has been created, and now we have 1 soldier(s)
- Soldier: 来不及解释了快上车!
A soldier has been created, and now we have 2 soldier(s)
- Soldier: 来不及解释了快上车!
- Enemy : Ao!!!!!!!
- Enemy : Ao!!!!!!!!!!!!!!!!!
Soldier No.2 gets injured, now his health is 250
- Soldier: 破机车发不动... 
- Enemy : Ao!!!!!!!!!!!!!!!!!
Soldier No.2 gets injured, now his health is 138
- Soldier: 破机车发不动... 
- Enemy : Ao!!!!!!!!!!!!!!!!!
- Enemy : Ao!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Soldier No.1 gets injured, now his health is 170
- Soldier: 破机车发不动... 
- Enemy : Ao!!!!!!!!!!!!!!!!!
Soldier No.2 gets injured, now his health is 41
- Soldier: 破机车发不动... 
- Enemy : Ao!!!!!!!!!!!!!!!!!
Soldier No.2 gets injured, now his health is -56
- Soldier: 破机车发不动... 
Soldier No.2 has been dead, now we have 1 soldier(s)
- Soldier: 车...开走了吗
- Enemy : Ao!!!!!!!!!!!!!!!!!
- Enemy : Ao!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=====result=====

    You win!

================

看起来似乎效果不错:)

整体回顾


var GUID = function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0;
        var v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
}

if(typeof Object.create !== 'function') {
    Object.create = function (o) {
        var F = function () {};
        F.prototype = o;
        return new F();
    }
}

var Model = new Class;

Model.extend({
    createInstance: function () {
        var i = Object.create(this.prototype);
        i.parent = this;
        // i.init.apply(i, arguments);
        i.create.apply(i);
        i.initialState.apply(i, arguments);
        return i;
    },
    createClass: function (attrs) {
        var o = Object.create(this);
        o._records = {};
        o.parent = this;
        o.fn = o.prototype = Object.create(this.prototype);
        o.include(attrs);
        this.onCreated.apply(this);
        this.onInherited.call(this, o);
        return o;
    },
    addClassStatic: function (staticMethods) {
        for (var method in staticMethods) {
            this[method] = staticMethods[method];
        }
    },
    getRecord: function (id) {
        return this._records[id];
    },
    // callbacks
    onInherited : function () {},
    onCreated: function () {},
    onRecordCreated: function () {},
    onRecordChanged: function () {},
    onRecordDestroyed: function () {}
});

Model.include({
    init: function () {},
    create: function () {
        if(!this._id) this._id = GUID();
        this.parent._records[this._id] = this;
        this.proxy(this.onCreated(this));
        this.parent.onRecordCreated(this);
        this._state = {};
    },
    initialState: function (s) {
        if (typeof s !== 'object') {
            console.warn('Parameter of createInstance should be an object.');
            return;
        }
        this.deepCopy(s, this._state);
        this.proxy(this.onStateCreated(this));
    },
    getState: function (attr) {
        if (!this._state[attr]) {
            console.warn('State attribute %s is not available.', attr);
            return null;
        }
        return this._state[attr];
    },
    setState: function (stateDiff) {
        if (typeof stateDiff !== 'object') {
            console.error('Parameter of setState should be an object');
            return;
        }

        var oldState = {};

        for (var attr in stateDiff)
            oldState[attr] = this._state[attr];
            this._state[attr] = stateDiff[attr];

        // provide old state, wrap getState method to make it act like an old instance of Model
        oldState.getState = function (attr) {
            return oldState[attr];
        }

        if (stateDiff) this.parent.onRecordChanged(this, oldState);
        this.proxy(this.onStateChanged(this, oldState));
    },
    destroy: function () {
        this.proxy(this.onStateDestroyed(this));
        this.onDestroyed.apply(this);
        this.parent.onRecordDestroyed(this);

        delete this.parent._records[this._id];
    },
    deepCopy : function (resource, target) {
        if(typeof resource !== 'object') return resource;
        for (var i in resource) {
            target[i] = this.deepCopy(resource[i], target);
        }
        return target;
    },
    // callbacks
    onCreated: function () {},
    onStateCreated: function () {},
    onStateChanged: function () {},
    onStateDestroyed: function () {},
    onDestroyed: function () {}
});
Copyright (c) 2014-2016 Kyles Light.
Powered by Tornado.
鄂 ICP 备 15003296 号