美文网首页
第二章、MVVM模式原理

第二章、MVVM模式原理

作者: vinterx | 来源:发表于2019-03-17 23:24 被阅读0次

    一、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()
        }
      })
    }
    

    双向绑定流程分析
    注意

    • Dep.target就是Watcher的一个实例(watcher)
    • Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数。
      理解以上两点对下面讲解流程有很大帮助,这两点暂不在本文章中详解。

    依赖收集流程
    \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更新页面。

    相关文章

      网友评论

          本文标题:第二章、MVVM模式原理

          本文链接:https://www.haomeiwen.com/subject/fnyvuqtx.html