MVC

model,view,controller:模型、视图、控制器
Java: springMVC
PHP: smarty
Node: Express、Koa

modal:

在 mvc 中实际是数据模型的概念,可以把它当成从数据库中查询出来的一条数据,或者是将查询出的元数据经过裁剪或者处理后的一个特定的数据模型结构

controller:

是数据模型与 VIew 视图之间的桥梁层,实际界面层的各种变化都要经过他来控制,而且从界面提交的数据,也会经过 controller 的组装检查生成数据模型,然后改变数据库里的数据内容。

MVC 的使用

数据到页面的渲染:先要查询数据库后,将拿到的元数据进行一些处理,(删除无效字段,或者进行多个数据模型间的数据聚合,然后在给到模板引擎,进行数据组装),最后组合完成进行渲染后生成 HTML 格式文件供文件浏览器使用。
前后端分离 ==> view 层 ==> Restful 风格的接口 兴起
前后端分离之后,可以理解成整个系统在原先的 MVC 基础上 View 层进行细化,把真格前端项目当成一个 View 层,从前端角度看, Restful 接口返回的 JSON 数据当成一个数据模型,作为 MVC 的 Model 层。而前端 JavaScript 自身对数据的处理是 controller 层, 真正的页面渲染结果是 View 层。
下例以前端视角,MVC 模式。接口返回的数据 Model 模型与 View 页面之间由 controller 连接,来完成系统中的数据展示。

  1. <div>
  2. <span id="name"></span>
  3. <div id="data"></div>
  4. </div>
  1. // 生成 model 数据模型
  2. function getDataApi() {
  3. // 模拟 接口返回
  4. return {
  5. name: 'mvc',
  6. data: 'mvc 中的数据',
  7. }
  8. }
  9. // controller 控制逻辑
  10. function pageController() {
  11. const res = getDataApi()
  12. $('#name').text(`姓名:${res.name}`)
  13. $('data').text(res.data)
  14. }

MVVM

前端对于逻辑的控制越来越清凉,mvvm 模式作为 mvc 模式的一种补充出现了,最终的目的都是将Model里的数据展示到view视图上,而 mvvm 相比于 mvc 则将前端开发所要控制的逻辑更加轻量级。

概念:

MVVM 是 Model-View-ViewModel 的缩写。

  • M:Model 数据模型,也可在 Model 中定义数据修改和操作业务逻辑。
  • V:view 代表UI组件,负责将数据模型转化成UI呈现出来。
  • VM:ViewModel 监听数据模型的改变和控制试图行为、处理用户交互,简单理解就是一个同步 VIew 和 Model 的对象。连接 Model 和 View。

在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

ViewModel

将模型与视图做了一层绑定关系,在理想情况下,数据模型返回什么视图就应该展示什么。

  1. <div>
  2. <span vm-bind-key="name"></span>
  3. <div vm-bind-key="data"></div>
  4. </div>
  1. // 生成数据 model 模型
  2. function getDataApi() {
  3. // 模拟接口返回
  4. return {
  5. name: 'mvc',
  6. data: 'mvc 的数据信息',
  7. }
  8. }
  9. // viewModel 控制逻辑
  10. function pageViewModel() {
  11. const res = getDataApi()
  12. return res
  13. }

理想情况下,在 viewModel 引入之后,视图完全由接口返回数据驱动。
在 mvvm 模式下,controller 控制逻辑不是没有了,想操作 DOM 相应的逻辑被 SDK(vue 内部封装实现)统一实现了,像不操作接口返回的数据是因为服务端在数据返回给前端前已经操作好了。
pageModel 函数实现了,如何将数据模型和页面视图绑定起来?

  1. Angular 的 主动轮询检查新旧值的变化更新视图
  2. Vue 利用 ES5 的 OBject.defineProperty 的 getter/setter 方法绑定,
  3. backbone 的发布订阅模式

从主动和被动的方式,实现了 viewModel 的关系绑定。

Vue2.0 MVVM 实现

当 Object.defineProperty 方法在给数据 Model 对象定义属性的时候先挂载一些方法,在这些方法里实现与界面的值的绑定响应关系,当应用的属性被读取或者写入的时候便会触发这些方法,从而达到数据模型里的值发生变化时同步响应到页面上。

  1. <div id="app">
  2. <span>{{name}}</span>
  3. <span>{{data}}</span>
  4. </div>
  5. <script src="vue.js"></script>
  1. // 生成 model 数据模型
  2. function getDataApi() {
  3. // 模拟接口返回
  4. return {
  5. name: 'mvc',
  6. data: 'mvc 数据信息',
  7. }
  8. }
  9. new Vue({
  10. el: '#app',
  11. data() {
  12. return {
  13. name: '',
  14. data: '',
  15. }
  16. },
  17. mounted() {
  18. const res = getDataApi()
  19. this.name = res.name
  20. this.data = res.data
  21. },
  22. })

当 Vue 在实例化的时候,首先将 data 方法里返回的对象属性都挂载上 setter 方法,而 setter 方法里将页面上的属性进行绑定,当页面加载时,浏览器提供 DOMContentloader 事件触发之后,调用 moutned 挂载函数,开始获取接口数据,获取完成后给 data 里的属性赋值,赋值的时候触发之前挂载好的 setter 方法,从而引起页面的联动,达到响应式效果。

简易实现 Object.defineProperty 的绑定原理

  1. <span id="name"></span>
  1. // Data Bindings
  2. Object.defineProperty(data, 'name', {
  3. get: function () {},
  4. set: function (newValue) {
  5. // 页面响应处理
  6. $('#name').text(newValue)
  7. data.name = newValue
  8. },
  9. enumerable: true,
  10. configurable: true,
  11. })
  12. // 页面DOM listener
  13. $('#name').onchange = function (e) {
  14. data.name = e.target.value
  15. }

Vue3.0 版本的 MVVM

Vue3.0 中最新的实现方式,用 Proxy 和 Reflect 来代替 Object.difineProperty。

Proxy

是 ES6 里的新构造函数,他的作用就是代理,简单理解为有一个对象,不想完全对外暴露出去,想做一层在亚原生对象操作之前的拦截、检查、代理,这时候就要考虑 Proxy。

  1. const myObj = {
  2. _id: '我是myObj里的Id',
  3. name: 'mvvm',
  4. age: 17,
  5. }
  6. const myProxy = new Proxy(myObj, {
  7. get(target, propKey) {
  8. if (proKey === 'age') {
  9. console.log('年龄私密,禁止访问!')
  10. return '*'
  11. }
  12. return target[propKey]
  13. },
  14. set(target, propKey, value, receiver) {
  15. if (propKey === '_id') {
  16. console.log('Id 无权修改')
  17. return
  18. }
  19. target[propKey] = value + (receiver.time || '')
  20. },
  21. // setPrototypeOf(target, proto) {},
  22. // apply(target, object, args) {},
  23. // construct(target, args) {},
  24. // defineProperty(target, propKey, propDesc) {},
  25. // deleteProperty(target, propKey) {},
  26. // has(target, propKey) {},
  27. // ownKeys(target) {},
  28. // isExtensible(target) {},
  29. // preventExtensions(target) {},
  30. // getOwnPropertyDescriptor(target, propKey) {},
  31. // getPrototypeOf(target) {},
  32. })
  1. myProxy._id = 23
  2. console.log(`age is:${myProxy.age}`)
  3. myProxy.name = 'my name is Proxy'
  4. console.log(myProxy)
  5. const newObj = {
  6. time: `[${new Date()}]`,
  7. }
  8. Object.setPrototypeOf(myProxy, newObj)
  9. console.log(myProxy.name)
  10. /**
  11. * id 无权修改
  12. * 年龄私密,禁止访问!
  13. * age is : *
  14. * {_id: '我是myObj的Id', name: 'my name is Proxy', age: 17}
  15. * my name is newObj [Thu Mar 19 2020 18:33:22 GMT+0800 (GMT+08:00)]
  16. */

Reflect

Reflect 是 ES6 里的新的对象,非构造函数,不能用 new 操作符。可以把它跟 Math 类比,Math 是处理 JS 中数学问题的方法函数集合,Reflect 是 JS 中对象操作方法函数集合,它暴露出来的方法与 Object 构造函数所带的静态方法大部分重合,实际功能也类似,Reflect 的出现一部分原因是想让开发者不直接使用 Object 这一类语言层面上的方法,还有一部分原因也是为了完善一些功能。Reflect 提供的方法还有一个特点,完全与 Proxy 构造函数里 Hander 参数对象中的钩子属性一一对应。

  1. const myObj = {
  2. _id: '我是myObj里的Id',
  3. name: 'mvvm',
  4. age: 17,
  5. }
  6. const myProxy = new Proxy(myObj, {
  7. get(target, propKey) {
  8. if (propKey === 'age') {
  9. console.log('年龄很私密,禁止访问')
  10. return '*'
  11. }
  12. return target[propKey]
  13. },
  14. set(target, propKey, value, receiver) {
  15. if (propKey === '_id') {
  16. console.log('id无权修改')
  17. return
  18. }
  19. target[propKey] = value + (receiver.time || '')
  20. },
  21. setPrototypeOf(target, proto) {
  22. if (proto.status === 'enable') {
  23. Reflect.setPrototypeOf(target, proto)
  24. return true
  25. }
  26. return false
  27. },
  28. })
  29. const newObj = {
  30. tmie: `[${new Date()}]`,
  31. status: 'disable',
  32. }
  33. // 原对象 原型链 赋值
  34. const result1 = Reflect.setPrototypeOf(myObj, {
  35. tmie: `[${new Date()}]`,
  36. status: 'sable',
  37. })
  38. myProxy.name = 'first set name'
  39. console.log(result1) // false
  40. console.log(myProxy.name) // first set name
  41. // 原对象 原型链 赋值
  42. const result2 = Reflect.setPrototypeOf(myProxy, {
  43. tmie: `[${new Date()}]`,
  44. status: 'sable',
  45. })
  46. myProxy.name = 'second set name'
  47. console.log(result2) // true
  48. console.log(myProxy.name) // //second set name [Thu Mar 19 2020 19:43:59 GMT+0800 (GMT+08:00)]
  49. /*当执行到这里时直接报错了*/
  50. // 原对象原型链赋值
  51. Object.setPrototypeOf(myProxy, {
  52. time: ` [${new Date()}]`,
  53. status: 'disable',
  54. })
  55. myProxy.name = 'third set name'
  56. console.log(myProxy.name)
  57. /**
  58. * 报错
  59. */

解释一下上面的这段代码,通过 Reflec.setPrototypeOf 方法修改原对象原型时,必须经过 Proxy 里 hander 的挂载的 setPrototypeOf 挂载函数,在挂载函数里进行条件 proto.status 是否是 enable 筛选后,再决定是否真正修改原对象 myObj 的原型,最后返回 true 或者 false 来告知外部原型是否修改成功。
这里还有一个关键点,就是在代码执行到原有的 Object.setPrototypeOf 方法时,程序则直接抛错,这其实也是 Reflect 出现的一个原因,即使现在 ES5 里的 Object 有同样的功能,但是 Reflect 实现的更友好,更适合开发者开发应用程序。
实现 MVVM
接下来使用上面的 Proxy 和 Reflect 来实现 MVVM,这里将 data 和 Proxy 输出到全局 Window 下,方便我们模拟数据双向联动的效果。

  1. <!DOCTYPE html>
  2. <html>
  3. <div>name: <input id="name" /> age: <input id="age" /></div>
  4. </html>
  5. <script>
  6. // 与页面绑定
  7. const data = {
  8. name: '',
  9. age: 0,
  10. }
  11. // 暴露到外部,便于查看效果
  12. window.data = data
  13. window.myProxy = new Proxy(data, {
  14. set(target, propKey, value) {
  15. // 改变数据 Model 时修改页面
  16. if (propKey === 'name') {
  17. document.getElementById('name').value = value
  18. } else if (propKey === 'age') {
  19. document.getElementById('age').value = value
  20. }
  21. Reflect.set(...arguments)
  22. },
  23. })
  24. // 页面变化改变 Model 内数据
  25. document.getElementById('name').onchange = function (e) {
  26. Reflect.set(data, 'name', e.target.value)
  27. }
  28. document.getElementById('age').onchange = function (e) {
  29. Reflect.set(data, 'age', e.target.value)
  30. }
  31. </script>

先打印了 data,然后模拟有异步数据过来,手动修改 data 里的数据 window.myProxy.age=25,这时候页面上的 age 联动变化为 25,再次打印了查看 data。接下来在页面上手动输入 name,输入完成后触发输入框的 onchange 事件后,再次查看 data,此时 model 里的数据已经变化为最新的与页面保持一致的值。