第二章、MVVM模式原理

2019-03-17  本文已影响0人  vinterx

一、MVVM和MVC模式的区别

讲到MVVM模式和MVC模式的区别,网上一大堆讲解的,我只简单讲解一下,MVC模式(Model-View-Controller)就是单向数据流。View ==> Controller ==> Model ==> View 形成闭环,视图层和数据层没有完全分离,View里面包含了Model的一些信息,数据直接驱动视图层变更,但是视图必须通过Controller来改变数据状态。

二、MVVM

Model <==> ViewModel <==> View
演变史:MVC ==> MVP ==> MVVM

  1. 低耦合高内聚
  2. 可重用性
  3. 独立开发
  4. 可测试

目前前端三大框架Vue、React、angular都是运用MVVM模式

三、双向数据绑定

\color{red}{Vue的v-model和Angular的ng-model双向数据绑定},双向数据绑定原理:单向绑定 + 事件监听
例如:

//  vue中
<input type="text" :value="meg" @input="meg=$event.target.value">  //  单向+监听事件

所以单向绑定也可以转化为双向数据绑定,只不过数据不会自动更新,需要监听事件(如oninput或onchange等)后主动改变数据。\color{red}{React就是单向数据绑定的。}

四、双向数据绑定的原理介绍

实现双向数据绑定大致有如下几种

  1. 发布-订阅者模式(backbone.js)
  2. 脏值检查(angular.js)
  3. 数据劫持(vue.js)

Vue的数据劫持
Vue双向数据绑定的核心是运用了Object.defineProperty(),Vue会遍历当前实例的data里面的数据属性,通过Object.defineProperty()设置为访问器属性,然后在该属性的get函数中将其设置为watcher,在set函数中向其他watcher发布改变的消息。
配合发布/订阅者模式,改变其中的某个值,所有的watcher会更新自己,这些watcher也就是绑定dom中的显示信息。
watcher.js

import { pushTarget } from './dep'

export default class Watcher {
  getter
  cb

  constructor (expOrFn, depFn) {
    this.cb = depFn
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = () => {
        if (/\./.test(expOrFn)) {
          let helpData = data
          expOrFn.split('.').forEach((path) => {
            helpData = helpData[path]
          })
          return helpData
        }
        return data[expOrFn]
      }
    }
    this.get()
  }

  get () {
    pushTarget(this)
    this.getter()
  }

  addDep (dep) {
    dep.addSub(this)
  }

  update () {
    this.getter()
    this.cb && this.cb()
  }
}

dep.js

export default class Dep {
  subs = []

  constructor () {
  }

  depend () {
    Dep.target.addDep(this)
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Dep.target = null

export function pushTarget (_target) {
  Dep.target = _target
}

observer.js(理解)
以下是简写,当然是存在一些问题,但是有助于理解。

import Dep from './dep'

    //  obj是data方法里面的属性,vm是vue实例
function observer(obj, vm){
    Object.keys(obj).forEach(key => {
        defineReactive(vm, key, obj[key])
    })
}

    //  设置访问器属性
function defineReactive(obj, key, val){
    let dep = new Dep()

    Object.defineProperty(obj, key, {
        get() {
            // Dep.target 是全局变量指向当前正在解析指令Complie生成的watcher实例
            //  将wtacher添加到每个dep实例中,即每个dep实例都有watcher观察者。
            if(Dep.target) {
                dep.addSub(Dep.target)
            }
            return val
        },
        set(newVal) {
            if(newVal === val) return
            val = newVal

            //  发送通知给订阅者列表
            dep.notify()
        }
    })
}

observer.js(源码)

// ......
import Dep from './dep'

// ......

export function defineReactive (obj, key, val) {
  const dep = new Dep();

  // ......

  Object.defineProperty(obj, key, {
    get () {
      // ......
      // 替代原来收集依赖的方式
      dep.depend()
      // ......
    },
    set (newVal) {
      // ......
      // 替代原来触发依赖执行的方式
      dep.notify()
    }
  })
}

双向绑定流程分析
注意

依赖收集流程
\color{red}{observer} ==>
\color{red}{walk}(这部分没有附上代码,当data数据类型不是对象或者数组的时候就会执行这个walk方法) ==>
\color{red}{defineReactive}(这个方法容易理解,就是将所有数据属性变成访问器属性)==>
\color{red}{get}(Watcher构造函数中也有个get方法,这个get方法在初始Watcher构造函数(data初始化)时会执行一次的,watcher.getter(),执行首次页面渲染) ==>
\color{red}{dep.depend()} ==>
\color{red}{Dep.target.addDep(this)}(watcher.addDep(new Dep()))==>
\color{red}{watcher.newDeps.push(dep)}(这个newDeps是watcher的方法,起到缓存作用,具体还待研究) ==>
\color{red}{dep.addSub(new Watcher())} ==>
\color{red}{dep.subs.push(watcher)}

视图更新流程
\color{red}{set}(数据发生变化时触发) ==>
\color{red}{dep.notify() }==>
\color{red}{subs[i].update()}(当前数据的所有watcher都会调用watcher.update()) ==>
\color{red}{watcher.run()} ==>
\color{red}{watcher.get() || watcher.cb}(触发Compile中绑定的回调) ==>
\color{red}{watcher.getter()}(初始化也会调用这个方法,依赖收集流程有介绍) ==>
\color{red}{vm.\_update()}(Vue实例更新) ==>
\color{red}{vm.\_patch\_}(DOM更新完成)

总结:
通过Object.defineProperty()将data中的每个数据属性转变为访问器属性,在依赖收集时watcher的newDeps数组存了dep,同时dep的subs数组也存了watcher,这就做到了数据变化,遍历dep的subs数组中的每个watcher,因为watcher中也存了dep,能检测到对应的那个数据变化,从而触发compile的callback更新页面。

上一篇下一篇

猜你喜欢

热点阅读