MVVM

MVVM 由以下三个内容组成

  • View:界面
  • Model:数据模型
  • ViewModel:作为桥梁负责沟通 View 和 Model

在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。

在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。

脏数据检测

当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化。循环至少为二次 ,至多为十次。

脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,都可以完成任务,但是这在 Vue 中的双向绑定是存在问题的。并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,大大减少了操作 DOM 的次数。所以低效也是相对的,这就仁者见仁智者见智了。

数据劫持

Vue 内部使用了 Object.defineProperty() 来实现双向绑定,通过这个函数可以监听到 setget 的事件。

  1. var data = { name: 'yck' }
  2. observe(data)
  3. let name = data.name // -> get value
  4. data.name = 'yyy' // -> change value
  5. function observe(obj) {
  6. // 判断类型
  7. if (!obj || typeof obj !== 'object') {
  8. return
  9. }
  10. Object.keys(obj).forEach(key => {
  11. defineReactive(obj, key, obj[key])
  12. })
  13. }
  14. function defineReactive(obj, key, val) {
  15. // 递归子属性
  16. observe(val)
  17. Object.defineProperty(obj, key, {
  18. enumerable: true,
  19. configurable: true,
  20. get: function reactiveGetter() {
  21. console.log('get value')
  22. return val
  23. },
  24. set: function reactiveSetter(newVal) {
  25. console.log('change value')
  26. val = newVal
  27. }
  28. })
  29. }

以上代码简单的实现了如何监听数据的 setget 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

  1. <div>
  2. {{name}}
  3. </div>
  1. 在解析如上模板代码时,遇到 `{{name}}` 就会给属性 `name` 添加发布订阅。
  1. // 通过 Dep 解耦
  2. class Dep {
  3. constructor() {
  4. this.subs = []
  5. }
  6. addSub(sub) {
  7. // sub 是 Watcher 实例
  8. this.subs.push(sub)
  9. }
  10. notify() {
  11. this.subs.forEach(sub => {
  12. sub.update()
  13. })
  14. }
  15. }
  16. // 全局属性,通过该属性配置 Watcher
  17. Dep.target = null
  18. function update(value) {
  19. document.querySelector('div').innerText = value
  20. }
  21. class Watcher {
  22. constructor(obj, key, cb) {
  23. // 将 Dep.target 指向自己
  24. // 然后触发属性的 getter 添加监听
  25. // 最后将 Dep.target 置空
  26. Dep.target = this
  27. this.cb = cb
  28. this.obj = obj
  29. this.key = key
  30. this.value = obj[key]
  31. Dep.target = null
  32. }
  33. update() {
  34. // 获得新值
  35. this.value = this.obj[this.key]
  36. // 调用 update 方法更新 Dom
  37. this.cb(this.value)
  38. }
  39. }
  40. var data = { name: 'yck' }
  41. observe(data)
  42. // 模拟解析到 `{{name}}` 触发的操作
  43. new Watcher(data, 'name', update)
  44. // update Dom innerText
  45. data.name = 'yyy'

接下来,对 defineReactive 函数进行改造

  1. function defineReactive(obj, key, val) {
  2. // 递归子属性
  3. observe(val)
  4. let dp = new Dep()
  5. Object.defineProperty(obj, key, {
  6. enumerable: true,
  7. configurable: true,
  8. get: function reactiveGetter() {
  9. console.log('get value')
  10. // 将 Watcher 添加到订阅
  11. if (Dep.target) {
  12. dp.addSub(Dep.target)
  13. }
  14. return val
  15. },
  16. set: function reactiveSetter(newVal) {
  17. console.log('change value')
  18. val = newVal
  19. // 执行 watcher 的 update 方法
  20. dp.notify()
  21. }
  22. })
  23. }

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。

Proxy 与 Object.defineProperty 对比

Object.defineProperty 虽然已经能够实现双向绑定了,但是他还是有缺陷的。

  1. 只能对属性进行数据劫持,所以需要深度遍历整个对象
  2. 对于数组不能监听到数据的变化

虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。

  1. const arrayProto = Array.prototype
  2. export const arrayMethods = Object.create(arrayProto)
  3. // hack 以下几个函数
  4. const methodsToPatch = [
  5. 'push',
  6. 'pop',
  7. 'shift',
  8. 'unshift',
  9. 'splice',
  10. 'sort',
  11. 'reverse'
  12. ]
  13. methodsToPatch.forEach(function (method) {
  14. // 获得原生函数
  15. const original = arrayProto[method]
  16. def(arrayMethods, method, function mutator (...args) {
  17. // 调用原生函数
  18. const result = original.apply(this, args)
  19. const ob = this.__ob__
  20. let inserted
  21. switch (method) {
  22. case 'push':
  23. case 'unshift':
  24. inserted = args
  25. break
  26. case 'splice':
  27. inserted = args.slice(2)
  28. break
  29. }
  30. if (inserted) ob.observeArray(inserted)
  31. // 触发更新
  32. ob.dep.notify()
  33. return result
  34. })
  35. })

反观 Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象进行拦截,所以 Vue 也将在下个大版本中使用 Proxy 替换 Object.defineProperty

  1. let onWatch = (obj, setBind, getLogger) => {
  2. let handler = {
  3. get(target, property, receiver) {
  4. getLogger(target, property)
  5. return Reflect.get(target, property, receiver);
  6. },
  7. set(target, property, value, receiver) {
  8. setBind(value);
  9. return Reflect.set(target, property, value);
  10. }
  11. };
  12. return new Proxy(obj, handler);
  13. };
  14. let obj = { a: 1 }
  15. let value
  16. let p = onWatch(obj, (v) => {
  17. value = v
  18. }, (target, property) => {
  19. console.log(`Get '${property}' = ${target[property]}`);
  20. })
  21. p.a = 2 // bind `value` to `2`
  22. p.a // -> Get 'a' = 2