软件工程课程总结

经过一学期的高级软件工程课程的学习,收获非常多,从“工欲善其事,必先利其器”中了解到非常强大的开发工具和基础技能,到“代码中的软件工程——工程化编程实战中”编写Menu程序体会到了何为工程化的编程,到“码农的自我修养之从需求分析到软件设计”学习到了需求分析和软件设计的方法,再到“码农的自我修养之软件科学基础概论”学习了软件架构及其描述方法和设计模式,最后在“码农的自我修养之软件危机和软件过程”体会到了没有银弹的含义等等,学到了很多,在此对课程中所讲的MVVM模式以vue.js代码为例进行总结。

 

在此之前曾多次在网上搜索有关MVVM的描述,却一致无法深入理解,直到在课堂上听了孟宁老师的讲解,感觉豁然开朗。

一、什么是MVVM?

MVVM即 Model-View-ViewModel,最早由微软提出来,借鉴了桌面应用程序的MVC模式的思想,是一种针对WPF、Silverlight、Windows Phone的设计模式,目前广泛应用于复杂的Javacript前端项目中。

二、Vue.js框架的MVVM架构

在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。以比较流行的Vue.js框架为例,MVVM架构示意图如下:

三、Vue.js的基本用法

1. Model、View在vue.js中的表示

Model是一个JavaScript对象 

{
    message: 'Hello Vue!'
}

负责显示的是DOM节点可以用{{ message }}来引用Model的属性,也就是View了 

<div id="app">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>

 其中v-on:click="reverseMessage"用来跟踪DOM的点击事件调用reverseMessage方法

methods: {
    reverseMessage: function () {
        this.message = this.message.split('').reverse().join('')
    }
}

2. vue中Model与View的绑定

<!DOCTYPE html>
<html>
<head>
  <title>My first Vue app</title>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <button v-on:click="reverseMessage">Reverse Message</button>
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue.js!'
      },
      methods: {
        reverseMessage: function () {
          this.message = this.message.split('').reverse().join('')
        }
      }
    })
  </script>
</body>
</html>

我们在创建Vue对象的时候将View的id="app"与Model(JavaScript对象定义的data)绑定起来,这样'Hello Vue!'就会自动更新到View DOM元素中。View DOM元素button上的事件click绑定Vue对象的方法reverseMessage,这样点击button按钮就能触发reverseMessage,reverseMessage方法只是修改了Model中JavaScript对象定义的message,而页面却能神奇地自动更新message。就是模型数据绑定和DOM事件监听。如下完整代码来自https://vuejs.org/v2/guide/,该页面上也有如下代码的运行演示。

四、Vue.js背后MVVM模型的秘密

Vue.js是一个前端构建数据驱动的Web界面的库,主要的特色是响应式的数据绑定,区别于以往的命令式用法。也就是在this.message = this.message.split('').reverse().join('')的过程中,拦截'='的过程,从而实现模型和视图自动同步更新的功能。而不需要显式地使用命令更新视图。Vue.js如何做到这一点的呢?

1. Object.defineProperty

首先把一个普通对象作为参数创建Vue对象时,Vue.js将遍历data的属性,用 Object.defineProperty 将要观察的对象“=”操作转化为getter/setter,以便拦截对象赋值与取值操作,称之为Observer;

//遍历data用Object.defineProperty 将要观察的对象“=”操作转化为getter/setter
Observer.prototype.transform = function(data){
    for(var key in data){
        var value = data[key];
        Object.defineProperty(data, key, {
            enumerable:true,
            configurable:true,
            get:function(){
                return value;
            },
            set:function(newVal){
                if(newVal == value){
                    return;
                }
                //遍历newVal
                this.transform(newVal);
                data[key] = newVal;//赋值还会调用set方法死循环了
            }
        });

        //递归处理
        this.transform(value);
    }
};

此处代码涉及到的点有:闭包和递归。js中的对象默认都有set、get方法,此处为重载;在set中对data[key]赋值还会点用set,有死循环,所以在这里要是用闭包进行处理;因为value可能还是key:value,所以涉及到递归;

 2. 编译视图模板的主要工作

 将DOM解析,提取其中的事件指令与占位符/表达式,并赋与不同的操作创建Watcher在模型中监听视图中出现的占位符/表达式,以及根据事件指令绑定监听事件和method,这是编译视图模板的主要工作,我们称之为Compiler;

//DOM中的指令与占位符
...
    <p>{{ message }}</p>
    <button v-on:click="reverseMessage">Reverse Message</button>
...

//创建Watcher在模型中监听视图中出现的占位符/表达式的每一个成员
var watcher = new Watcher("message"):
//绑定监听事件和method
node.addEventListener("click", "reverseMessage"):

3. 使用到观察者模式

1)绑定观察者及get方法调用

将Compiler的解析结果,与Observer所观察的对象连接起来建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM,称之为Watcher;

//观察者模式中的被观察者的核心部分
var Dep = function(){
    this.subs = {};
};
Dep.prototype.addSub = function(target){
    if(!this.subs[target.uid]) {
        //防止重复添加
        this.subs[target.uid] = target;
    }
};
Dep.prototype.notify = function(newVal){
    for(var uid in this.subs){
        this.subs[uid].update(newVal);
    }
};
Dep.target = null;

代码分析:Dep即为被观察者,即Model中数据的一个属性,在subs中记录了所有的观察者。其中的target即为编译视图模版得到的watcher,通过addSub将观察者watcher无重复的添加到subs中,在数据变化时通过notify通知所有的观察者,此处调用的updata函数在如下代码段中定义:

//创建Watcher,观察者模式中的观察者
var Watcher = function(exp, vm, cb){
    this.exp = exp; // 占位符/表达式的一个成员
    this.cb = cb; //更新视图的回调函数
    this.vm = vm; //ViewModel
    this.value = null;
    this.getter = parseExpression(exp).get;
    this.update();
};

Watcher.prototype = {
    get : function(){
        Dep.target = this;
        var value = this.getter?this.getter(this.vm):'';
        Dep.target = null;
        return value;
    },
    update :function(){
        var newVal = this.get();
        if(this.value != newVal){
            this.cb && this.cb(newVal, this.value);
            this.value = newVal;
        }
    }
};

首先对占位符/表达式进行绑定,exp是在编译视图阶段传入的:new Watcher("message")。由于watcher是编译视图模版得出来的,watcher是视图的一部分,所以此处的this.value是视图中的value,一开始被设置为null。然后将this.getter指向被观察的exp的get方法,最后点用updata。

对于update函数,首先会调用this.get方法获取新的值,此处的newVal是Model中的数据。get方法首先将Dep.target指向自身,然后调用this.getter方法获取被观察者的值,方法实现如下:

Observer.prototype.defineReactive = function(data, key, value){
    var dep = new Dep();
    Object.defineProperty(data, key ,{
        enumerable:true,
        configurable:false,
        get:function(){
            if(Dep.target){
                //添加观察者
                dep.addSub(Dep.target);
            }
            return value;
        },
        set:function(newVal){
            if(newVal == value){
                return;
            }
            //data[key] = newVal;//死循环!赋值还会调用set方法
            value = newVal;//为什么可以这样修改?闭包依赖的外部变量
            //遍历newVal
            this.transform(newVal);
            //发送更新通知给观察者
            dep.notify(newVal);
        }
    });

    //递归处理
    this.transform(value);
};

 进入exp的get方法后,首先将watcher添加到观察者列表中,然后将值返回,如果当前视图中的值与Model中的值不相等,update就调用更新视图的回调函数cb来更新视图,并将newVal赋值给this.Value。

因为是在取值操作时将watcher加入到观察者列表中,所以每次取值都会尝试添加,所以在前面介绍的Dep.addSub中会先判断是否已经添加过,防止重复添加。

2)set方法调用

methods: {
    reverseMessage: function () {
        this.message = this.message.split('').reverse().join('')
    }
}

当Model中的数据被修改时,复制操作“=”会被拦截,触发对应exp的set方法,前面说到set方法中有死循环,修改后的代码如下:

Observer.prototype.defineReactive = function(data, key, value){
    var dep = new Dep();
    Object.defineProperty(data, key ,{
        enumerable:true,
        configurable:false,
        get:function(){
            if(Dep.target){
                //添加观察者
                dep.addSub(Dep.target);
            }
            return value;
        },
        set:function(newVal){
            if(newVal == value){
                return;
            }
            //data[key] = newVal;//死循环!赋值还会调用set方法
            value = newVal;//为什么可以这样修改?闭包依赖的外部变量
            //遍历newVal
            this.transform(newVal);
            //发送更新通知给观察者
            dep.notify(newVal);
        }
    });

    //递归处理
    this.transform(value);
};

 在这里使用到了闭包,直接对value进行赋值,即对闭包依赖的外部变量进行赋值。

set方法首先判断新值是否等于旧值,相等就直接返回,否则就修改Model的值,并通知观察者更新视图中的数据。

这样逻辑完整Vue.js内部实现的MVVM框架实现机制就呈现出来了。 

3)几个关键点

1. new vue时先transform对象再编译视图模版,因为new watcher时需要用到get。

2. get、set方法在transform中没有被执行,是一个闭包,set在赋值时才会被执行,get方法在取值时才会被执行。

 

 

作者:SA21225444

参考资料 代码中的软件工程 https://gitee.com/mengning997/se

学到了很多新知识,对软件工程的理解更深刻了,在此非常感谢孟宁老师的教导!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值