MVVM以及响应式数据原理

580 阅读3分钟

1. MVVM

MVVM其实表示的是 Model-View-ViewModel

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交互
  • View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
  • ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁

在MVVM的架构下,View层和Model层并没有直接联系,而是通过ViewModel层进行交互。 ViewModel层通过双向数据绑定将View层和Model层连接了起来,使得View层和Model层的同步工作完全是自动的。

主要就是 mvc 中 Controller 演变成 mvvm 中的 viewModel。mvvm 主要解决了 mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到 View 。

Vue 与 MVVM

Vue 框架其实就是起到 MVVM 模式中的 ViewModel 层的作用。

ViewModel层通过双向数据绑定将View层和Model层连接了起来,使得View层和Model层的同步工作完全是自动的。

2. 双向绑定(Vue 2.0)

vue.js是采用的数据劫持结合发布者-订阅者模式的方式,通过object.defineProperty()来劫持各个属性的setter/getter
在数据变动时,发布消息给订阅者,触发相应的监听回调

具体步骤:
    1)需要observe(观察者)的数据对象进行遍历,包括子属性对象的属性,都加上setter和getter,这样的话,
    给这个对象的某个值赋值,就会触发setter,那么就能监听到数据的变化
    2)compile(解析)解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,
    添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
    3)watcher(订阅者)是observer和compile之间通信的桥梁,主要做的事情是
        1>在实例化时往属性订阅器(dep)里面添加自己
        2>自身必须有一个update()方法
        3>待属性变动dep.notice()通知时,能够调用自身的update()方法,并触发compile中绑定的回调,
    4)mvvm作为数据绑定的入口,整合observer,compile和watcher来监听自己的model数据变化,通过compile来解析编译模版,
    最终利用watcher搭起observer和compile之间的通信桥梁,达到数据变化->更新视图:视图交互变化->数据model变更的双向绑定效果

对象的变化侦测
//添加wather
function Watcher(vm, prop, callback) {
  this.vm = vm
  this.prop = prop
  this.callback = callback

  Watcher.prototype.update = function () {
    const value = this.vm.$data[this.prop]
    const oldVal = this.value
    if (value !== oldVal) {
      this.value = value
      this.callback(value)
    }
  }

  Watcher.prototype.get = function () {
    Dep.target = this //储存订阅器
    const value = this.vm.$data[this.prop] //因为属性被监听,这一步会执行监听器里的 get方法
    Dep.target = null
    return value
  }

  this.value = this.get()
}

//收集依赖
function Dep() {
  this.subs = []
  Dep.prototype.addSub = function (sub) {
    this.subs.push(sub)
  }
  Dep.prototype.notify = function () {
    console.log('属性变化通知 Watcher 执行更新视图函数')
    this.subs.forEach(function (sub) {
      sub.update()
    })
  }
}


class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()

    // 给value新增一个__ob__属性,值为该value的Observer实例
    // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
    // 最主要的原因:数组的dep最放在__ob__中,更新时会通知
    // 对象的新增属性或者删除属性this.$set()无法触发defineProperty中的dep,所以需要触发__ob__中的dep
    def(value, '__ob__', this) 
    if (Array.isArray(value)) {
      //将数组的拦截器添加至原型上
      value.__proto__ = arrayMethods
      for (let i = 0, l = value.length; i < l; i++) {
        //如果数组中含有数组,则每个数组的原型上都有一个__ob__,然后统一在defineProperty的get方法中去搜集watcher
        observe(value[i])
      }
    } else {
      Object.keys(this.value).forEach((key) => {
        defineReactive(value, key)
      })
    }
  }
}
function observe(value) {
  if (!isObject(value)) {
    return
  }
  let ob = new Observer(value)
  return ob
}
function defineReactive(obj, key, value) {
  const dep = new Dep()
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    configurable: true, //描述属性是否配置,以及可否删除(其实就是说可否再使用defineproperty配置此属性)
    enumerable: true, //描述属性是否会出现在for in 或者 Object.keys()的遍历中
    get: function () {
      // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function (newVal) {
      if (newVal === value) {
        return
      }
      //set是一个新的对象或者数组的时候,需要重新observe
      childOb = !shallow && observe(newVal)
      dep.notify() // 通知所有订阅者
    },
  })
}



数组
    // 数组原型对象
    const arrayProto = Array.prototype
    // 拦截器
    const arrayMethods = Object.create(arrayProto)
    debugger
    ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
      (method) => {
        const origin = arrayProto[method]
        console.log(method)
        Object.defineProperty(arrayMethods, method, {
          enumerable: false,
          writable: true,
          configurable: true,
          value: function mutator(...args) {
            console.log('现在开始拦截')
            const ob = this.__ob__
            // notify change
            ob.dep.notify()
            return origin.apply(this, args)
          },
        })
      }
    )
    
    
    function Observe(data) {
      if (!data || typeof data !== 'object') {
        return
      }

      if (Array.isArray(data)) {
        data.__proto__ = arrayMethods
        observeArray(data)
      } else {
        Object.keys(data).forEach((key) => {
          defineReactive(data, key, data[key])
         })
      }
    }
    
    observeArray(items) {
      for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
      }
    }

由于我们的拦截器是挂载到数组数据的原型上的,所以拦截器中的this就是数据value,拿到value上的Observer类实例,从而你就可以调用Observer类实例上面依赖管理器的dep.notify()方法,以达到通知依赖的目的。

3. 数据绑定(Vue 3.0)

优势:

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

Object.defineProperty 的优势:

兼容性好

    let proxy = new Proxy(data, {
      get: function (target, key) {
        console.log('监听到获取' + key + target)
        return target[key]
      },
      set: function (target, key, newvalue) {
        console.log('监听到变化' + key + target)
        target[key] = newvalue
        return true
      },
    })

4. 当前组件模板中用到的变量一定要定义在data里么

data中的变量都会被代理到当前this下,所以我们也可以在this下挂载属性,只要不重名即可。而且定义在data中的变量在vue的内部会将它包装成响应式的数据,让它拥有变更即可驱动视图变化的能力。但是如果这个数据不需要驱动视图,定义在created钩子内也是可以的,因为不会执行响应式的包装方法,对性能也是一种提升。(但不能定义在mounted中,因为此时已经挂在结束,找不到该属性会报错)

(this下声明属性是不会被添加watcher的,所以变化的时候侦测不到,此时可以通过改变data里面的数据来达到更新目的)

5. data中的变量如何被代理到当前this下

Vue.prototype.initData = function (data) {
    var keys = Object.keys(data);
    var i = keys.length;
    while (i--){
        const key = keys[i];
        this.proxy("$data",key);
    }
}

Vue.prototype.proxy =function(sourceKey, key) {
    Object.defineProperty(this, key, {
        get() {
            return this[sourceKey][key]
        },
        set(val){
            this[sourceKey][key] = val;
        }
    });
};

6. 当对象被重新赋值的时候,会重新obersve所有属性

源码:childOb = !shallow && observe(newVal);判断如果是对象的重新赋值会递归所有属性进行defineProperty

    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    },