• Javascript MVC 学习笔记(一) 模型和数据


    写在前面

    近期在看《MVC的Javascript富应用开发》一书。本来是抱着一口气读完的想法去看的。结果才看了一点就傻眼了:太多不懂的地方了。

    仅仅好看一点查一点,一点一点往下看吧,进度虽慢但也一定要坚持看完。

    本学习笔记是对书上所解说内容的理解和记录。
    笔记里的代码大多会按书上摘录下来,由于《MVC的Javascript富应用开发》是结合了JQuery库。所以对于JQuery中不太懂的知识点也会附在代码后面,也算是一些额外的收获。

    MVC概述

    要学习MVC,首先得知道MVC是什么。MVC有三个字母,代表了它将应用划分为了三个部分:数据(模型Model)、展现层(视图View)和用户交互层(控制器Controller)
    对于採用MVC模式搭建的应用,一个事件的发生是这种过程:

    1. 用户和应用产生交互
    2. 控制器的事件处理器被触发
    3. 控制器从模型中请求数据,并将其交给视图
    4. 视图将数据呈现给用户

    MVC的目的在于将M和V的实现代码分离,从而使同一个程序能够使用不同的表现形式。

    介绍完MVC,就開始分别学习MVC三个组成部分了。

    模型

    模型用来存放应用的全部数据对象,它仅仅需包括数据以及直接和这些数据相关的逻辑。像其它什么事件监听、视图模版都应该隔离在模型之外。当控制器从server抓取数据或创建新的记录时,它就将数据包装成模型实例

    比方你的应用里可能有非常多用户相关的数据:比方从server拿到的用户的name、id。在应用里面,又会对用户进行加入、改动、删除。那么用户就应该成为你的一个模型。

    一个应用可能有多个模型(比方用户、商品),所以书中创建了一个Model对象,代表模型,对于不同的模型。将使用create方法创建,对于模型中详细的数据对象,使用init方法实例化。(听起来有点像类和对象),所以我们最后要实现的模型应该是以下的样子:

    //创建Asset模型
    var Asset = Model.create();
    //实例化一个Asset的对象
    var asset = Asset.init();
    asset.name = "same, same";
    asset.id = 1;
    asset.save();
    
    //实例化另一个Asset的对象
    var asset2 = Asset.init();
    asset2.name = "but different";
    asset2.id = 2;
    asset2.save();

    详细是怎么实现的先不要急,一步一步来看。

    构建对象关系映射(ORM)

    对象关系映射(Object-relational mapper)是一种常见的数据结构,本质上讲,ORM是一个包装了一些数据的对象层。比方上面的Asset,就是对用户数据的包装。
    首先,创建一个Model对象,Model对象用于创建新模型的实例。

    var Model = {
        //返回继承自Model的对象(创建一个新模型)
        create: function () {
            var object = Object.create(this);
            object.parent = this;
            object.prototype = object.fn = Object.create(this.prototype);
    
            return object;
        },
        //返回继承自Model的原型的对象(实例化一个模型)
        init: function () {
            var instance = Object.create(this.prototype);
            //实例能够通过parent属性訪问其Model
            instance.parent = this;
            //进行初始化
            instance.init.apply(instance, arguments);
            return instance;
        },
        //Model的原型对象
        prototype: {
            //初始化操作
            init: function () {
            }
        }
    }

    这个Model定义了create方法和init方法。像上面说的。create用于创建新模型。init方法用于创建一个模型实例,例如以下:

    var Asset = Model.create();
    var User = Model.create();
    
    var asset = Asset.init();
    var user = User.init();

    (注)Object.create

    上面代码重点在于Object.create()这种方法。这种方法返回一个对象。返回对象继承自方法接收的參数。假设没有參数。则继承自Object,如:

    var src = {
        name: "zhangsan"
    }
    var dest = Object.create(src);
    console.log(dest.name);     //"zhangsan"

    关于对象和其prototype对象之间的关系,能够查看前面的学习笔记:Javascript继承

    (注解完)

    加入ORM属性

    书中继续为Model创建了两个方法,extend和include。前者是为模型加入对象属性,后者为模型加入实例属性,例如以下:

    var Model = {
        /*省略create、init等方法*/
    
        //加入对象属性
        extend: function (o) {
            var extended = o.extended;
            //jQuery中extend的使用见后面注解
            $.extend(this, o);
            if (extended) {
                //假设參数有这个属性。将Model传给它并调用它(回调)
                extended(this);
            }
        },
        //加入实例属性
        include: function (o) {
            var included = o.included;
            $.extend(this.prototype, o);
            if (included) {
                included(this);
            }
        }
    }

    加入完之后。能够这样使用:

    Model.extend({
        find: function(){
            console.log("find");
        }
    })
    
    Model.include({
        init: function(){
            console.log("init");
        },
        load: function(){
            console.log("load");
        }
    });

    对象属性意思是直接通过模型訪问的,实例属性意思是通过实例訪问,比方:

    var Asset = Model.create();
    Asset.find();   //"find"
    var asset = Model.init();   //"init" Model的init方法中会调用实例的init方法。(见Model里init方法的实现)
    asset.init();   //"init"
    asset.load();   //"load"

    (注)jQuery.extend()

    extend方法能够将多个对象合并到一个对象中并返回。
    一、$.extend(dest, src1, src2, src3,…)
    将src1、src2以及后面的对象合并到dest对象中。返回合并后的dest(后面的对象中假设有前面对象内同名的属性。后面的对象属性值将覆盖前面的)

    var dest = {
        name: "张三",
        age: 20
    };
    
    var src1 = {
        school: "蓝翔"
    }
    
    var src2 = {
        name: "狗蛋",
        school: "新东方"
    }
    
    var obj = $.extend(dest, src1, src2);
    
    console.log(obj === dest);  //true
    console.log(obj);   //{"name":"狗蛋","age":20,"school":"新东方"}

    所以通过这种方法使用后dest的结构会改变。假设不想改变dest的结构。能够让第一个參数是一个空对象,即另外一种用法:

    二、$.extend({}, dest, src1, src2, …)

    var obj = $.extend({}, dest, src1, src2);
    
    console.log(obj);   //{"name":"狗蛋","age":20,"school":"新东方"}

    这样输出也是一样的,只是dest的结构没有改变。

    三、$.extend(src1)
    通过这种方式调用,src1将被合并到全局变量中去。

    $.extend(src1);
    console.log($.school); //"蓝翔"

    四、$.extend(boolean, dest, src1, src2)
    当extend第一个參数是一个布尔型的时候,它将代表是否要进行深度拷贝,true代表进行深度拷贝。即将对象的子对象也进行合并。

    当设置为false时。子对象将直接被覆盖。

    var dest = {
        name: "张三",
        age: 20,
        girlFriend: {
            name: "小丽",
            school: "清华"
        }
    };
    
    var src1 = {
        school: "蓝翔",
        girlFriend: {
            weight: 100,
            height: 165
        }
    }
    
    var obj = $.extend(false, dest, src1);
    console.log(obj.girlFriend);    //{"weight":100,"height":165}

    设置为true时,对子对象也进行合并

    var obj = $.extend(true, dest, src1);
    console.log(obj.girlFriend);    //{"name":"小丽","school":"清华","weight":100,"height":165}

    对于前三种用法,根本没有第一个參数。都没有进行深度拷贝。

    (注解完)

    回到主题,Model的extend方法中是jQuery.extend(this, o),所以是将參数合并到Model中,是Model的方法。include是jQuery.extend(this.prototype, o),所以是将參数合并到Model的原型对象上,是实例的方法。

    持久化记录

    我们如今能够创建多种模型。也能为一种模型创建多个实例,如今须要做的是将记录持久化,也就是将创建的实例保存起来。

    我们在Model中加入一个records对象用于保存实例,当创建一个实例时,指定其id,存入records内;当删除实例的时候。就将实例从records中删除。

    //用于保存资源的对象
    Model.records = {};
    
    //由于是实例的方法。所以调用include
    Model.include({
        //是否为新对象
        newRecord: true,
    
        //实例的创建
        create: function(){
            this.newRecord = false;
            //加入进Model的records中(实例能够通过parent属性訪问Model,见Model的init方法实现)
            this.parent.records[this.id] = this;
        },
    
        //实例的销毁
        destroy: function(){
            delete this.parent.records[this.id];
        },
    
        //更新实例
        update: function(){
            this.parent.records[this.id] = this;
        }
    })

    这样保存新实例的时候调用create。更新的时候调用update。为了方便,我们将其功能合并,使用save方法来保存实例,这样就不必推断是新实例还是已经保存过的实例了:

    Model.include({
        //保存实例
        save: function(){
            this.newRecode ? this.create() : this.update();
        }
    });

    最后。保存好实例后。我们须要通过模型来找到这个元素,为Model定义一个find方法,依据指定id寻找数据对象:

    Model.extend({
        //通过id寻找
        find: function(id){
            var record = this.records[id];
            if(!record){
                throw("Unkown record");
            }
            return record;
        }
    });

    至此。一个主要的ORM已经创建成功。我们能够试着执行一下:

    var Asset = Model.create();
    var asset = Asset.init();
    asset.name = "same, same";
    asset.id = 1;
    asset.save();
    
    var asset2 = Asset.init();
    asset2.name = "but different";
    asset.id = 2;
    asset.save();
    
    console.log(Asset.find("1").name);  //"same, same"
    asset2.name = "change";
    asset2.save();
    console.log(Asset.find("2").name);  //"change"

    添加id支持

    眼下我们每次保存记录的时候都是手动指定id。实在是非常糟糕的做法。但我们能够加入自己主动化处理,书中提供了一个生成随机树的GUID:(不明觉厉)

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

    这样有了生成一个随机字符串的方法,我们将其加入到create方法中:

    //实例的创建(见前面Model.include中代码)
    create: function(){
        //为其随机分配id
        if(!this.id){
            this.id = Math.guid();
        }
        this.newRecord = false;
        this.parent.records[this.id] = this;
    }

    这样便不用操心其id了:

    var asset = Asset.init();
    asset.save();
    console.log(asset.id);  //"A65AFEB6-CB51-44F4-9E54-77A617E69903"

    寻址引用

    我们的ORM事实上另一个问题,在Model的find方法中,我们直接返回了实例的引用。而不是它的复制。所以find之后对其属性的改动都会直接影响原始资源,这不是我们想要的。我们希望仅仅有当调用save/update方法时才改变资源的值:

    var asset = Asset.init();
    asset.name = "我是原始资源";
    asset.save();
    
    console.log(Asset.find(asset.id).name); //"我是原始资源"
    
    asset.name = "我改动了原始资源";
    console.log(Asset.find(asset.id).name); //"我改动了原始资源"

    asset的name被改动了,然而并没有调用save或update方法,为了避免这个问题,我们能够在find方法内创建一个新对象。并返回这个新对象。创建和更新也是一样:

    Asset.extend({
        //通过id寻找
        find: function(id){
            var record = this.records[id];
            if(!record){
                throw("Unkown record");
            }
            //返回复制的对象
            return record.dup();
        }
    });
    
    Asset.include({
        //实例的创建
        create: function(){
            this.newRecord = false;
            //复制对象
            this.parent.records[this.id] = this.dup();
        },
        //更新实例
        update: function(){
            this.parent.records[this.id] = this.dup();
        },
        //复制对象
        dup: function(){
            return $.extend(true, {}, this);
        }
    });

    初始化records

    另一个问题是records是被全部模型共享的对象,所以无论是Asset也好User也好都公用一个records,这当然是不能接受的。我们能够在一个模型初始化的时候为其初始化自己的records。

    Model.extend({
        create: function(){
            /*省略其它代码*/
            this.records = {};
        }
    })

    原书上是创建了一个created方法,在created方法内初始化records,created方法在create方法内调用。道理也是一样的。

    后面另一些为ORM加入本地存储的方法,ORM创建好了之后事实上道理都是一样的。就不再赘述了。明天继续看Controller。好好加油。

  • 相关阅读:
    [LeetCode] 210. Course Schedule II
    [LeetCode] 207. Course Schedule
    [LeetCode] 450. Delete Node in a BST
    [LeetCode] 1122. Relative Sort Array
    [LeetCode] 1013. Partition Array Into Three Parts With Equal Sum
    [LeetCode] 173. Binary Search Tree Iterator
    [LeetCode] 208. Implement Trie (Prefix Tree)
    [LeetCode] 211. Add and Search Word
    [LeetCode] 449. Serialize and Deserialize BST
    [LeetCode] 236. Lowest Common Ancestor of a Binary Tree
  • 原文地址:https://www.cnblogs.com/lytwajue/p/7359809.html
Copyright © 2020-2023  润新知