Vue 中的 MVVM

MVVM 框架前生 (MVC 框架)

在介绍 MVVM 框架 之前, 先让我们一起了解一下 MVC 框架

image
  • MVC 框架 将整个前端页面分成 View, Controller, Modal

  • 当视图发生变化, 通过 Controller(控件) 将响应传入到 Model(数据源) 中, 由数据源改变 View 上面的数据

  • 整个过程看起来是行云流水, 业务逻辑放在 Model 中, 页面渲染逻辑放在 View

什么是 “MVVM 框架”

image
image

可以看到 MVVM 分别指 View, Model, View-Model

View 通过 View-ModelDOM Listeners 将事件绑定到 Model

Model 则通过 Data Bindings 来管理 View 中的数据

View-Model 从中起到一个连接桥的作用

  • View 层: 也叫视图层, 在前端开发中, 通常就是 DOM 层, 主要的作用是给用户展示各种信息

  • Model 层: 也叫数据层, 数据可能是我们固定的死数据, 更多的是来自我们服务器, 从网络上请求下来的数据

  • Vue-Model 层: 也叫视图模型层, 视图模型层是 ViewModel 沟通的桥梁, 一方面实现了 Data Binding(数据绑定), 将 Model 的改变实时的反应到 View 中, 另一方面它实现了 DOM Listener(DOM监听), 当 DOM 发生一些事件 (点击、输入、滚动、touch等) 时, 可以监听到, 并在需要的情况下改变对应的 Data

MVVM 与 MVC 的区别

  • 如果你看对 MVC 框架 有所了解的话, 你会发现 MVC 框架 实际运用上却存在一个问题: 那就是 MVC 框架 允许 ViewModel 直接进行通信!!!

  • 这会造成 ViewModel 之间随着业务量的不断庞大, 会出现蜘蛛网一样难以处理的依赖关系, 完全背离了开发所应该遵循的 “开放封闭原则”

  • 面对这个问题, MVVM 框架 就出现了, 它与 MVC 框架 的主要区别以下两点:

    1. 实现数据与视图的分离
    2. 通过数据来驱动视图, 开发者只需要关心数据变化, DOM 操作被封装了

MVVM 原理

MVVM 的实现主要是三个核心点:

  1. 响应式: Vue 如何监听 data 的属性变化
  2. 模板解析: Vue 的模板是如何被解析的
  3. 渲染: Vue 模板是如何被渲染成 HTML 的

响应式

对于 MVVM 来说, data 一般是放在一个对象当中, 例如:

let obj = {
  rp: 0
}

当我们访问或修改 obj 的属性的时候, 例如:

console.log(obj.rp) // 访问
obj.rp = 10         // 修改

但是这样的操作 Vue 本身是没有办法感知到的, 那么应该如何让 Vue 知道我们进行了访问或是修改的操作呢?

那就要使用 Object.defineProperty

let VueModel = {}
let data = {
  name: "zhangsan",
  age: 20
}

for (let key in data) {
  Object.defineProperty(VueModel, key, {
    get: () => {
      console.log("get", data[key]) // 监听
      return data[key]
    },
    set: value => {
      console.log("set", value)    // 监听
      data[key] = value
    }
  })
}

通过 Object.defineProperty 将 data 里的每一个属性的访问与修改都变成了一个函数, 在函数 getset 中我们即可监听到 data 的属性发生了改变

模版解析

首先模板是什么?

模板本质上是一串字符串, 它看起来和 HTML 的格式很相像, 实际上有很大的区别, 因为模板本身还带有逻辑运算, 比如 v-if, v-for 等等, 但它最后还是要转换为 HTML 来显示

  • {{item}}

模板在 Vue 中必须转换为 JS 代码

  • 原因在于: 在前端环境下, 只有 JS 才是一个图灵完备语言, 才能实现逻辑运算, 以及渲染为 HTML 页面

这里就引出了 Vue 中一个特别重要的函数 —— render

render 函数中的核心就是 with 函数

  • with 函数将某个对象添加到作用域链的顶部

  • 如果在 statement 中有某个未使用命名空间的变量, 跟作用域链中的某个属性同名, 则这个变量将指向这个属性值

例如:

let obj = {
    name: "feng",
    age: 20,
    getAddress: () => {
        alert("shanghai")
    }
}

function fnc() {
    with(obj) {
        alert(age)
        alert(name)
        getAddress()
    }
}

fnc()

with 将 obj 这个对象放在了自己函数的作用域链的顶部, 当执行下列函数时, 就会自动到 obj 这个对象去寻找同名的属性

而在 render 函数中, with 的用法是这样

  • {{item}}

在一开始, 因为 new 操作符, 所以 this 指向了 app, 通过 with 我们将 app 这个对象放在作用域链的顶部, 因为在函数内部我们会多次调用 app 内部的属性, 所以使用 with 可以缩短变量长度, 提供系统运行效率

其中的 _c 函数表示的是创建一个新的 HTML 元素, 其基本用法为:

_c(element, { attrs }, [ children... ])
  • element 表示所要创建的 HTML 元素类型

  • attrs 表示所要创建的元素的属性

  • children 表示该 HTML 元素的子元素

  • _v 函数表示创建一个文本节点

  • _l 函数表示创建一个数组

  • 最终 render 函数返回的是一个虚拟 DOM

如何将模板渲染为 HTML

模板渲染为 HTML 分为两种情况

  1. 第一种是初次渲染的时候
  2. 第二种是渲染之后数据发生改变的时候, 它们都需要调用 updateComponent

其形式如下:

app._update(vnode) {
  const prevVnode = app._vnode
  app._vnode = vnode
  if (!prevVnode) {
    app.$el = app.__patch__(app.$el, vnode)
  } else {
    app.$el = app.__patch__(prevVnode, vnode)
  }
}

function updateComponent() {
  app._update(app._render())
}

首先读取当前的虚拟 DOM——app._vnode, 判断其是否为空

  • 若为空, 则为初次渲染, 将虚拟 DOM 全部渲染到所对应的容器当中(app.$el)
  • 若不为空, 则是数据发生了修改, 通过响应式我们可以监听到这一情况, 使用 diff 算法完成新旧对比并修改









参考文章: zhuanglog

你可能感兴趣的:(Vue 中的 MVVM)