MVVM原理

MVVM

是什么?

数据驱动视图
通过 ViewModel 在 Model(模型) 修改的时候触发 View(视图) 渲染,
View 中事件(点击等)修改 model 层数据

实现方式

1、发布订阅者
2、脏值检查
3、数据劫持

思路

1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
img

实现

<div id="app">
   <input type="text" v-model="message.a"/>
   {{message.a}}
 div>
let vm = new MVVM({
 el: '#app',
  data: {
    message: {
      a: 'hello'
    }
  }
})

MVVM

使用Vue的时候会new Vue,需要创建一个类,因此先初始化一个MVVM类

class MVVM {
  constructor (options) {
    // 一上来先将可用的东西挂载在实例上
    this.$el = options.el
    this.$data = options.data

    // 如果有要编译的模版就开始编译
    if (this.$el) {
      // 数据劫持 就是把对象方法里面的所有属性改成get和set
      new Observer(this.$data)
      this.proxyData(this.$data)
      // 用数据和元素进行编译
      new Compile(this.$el, this)
    }
  }
  proxyData (data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        get () {
          return data[key]
        },
        set (newValue) {
          data[key] = newValue
        }
      })
    })
  }
}

Observe

实现Oberve,监听属性的变化

class Observer {
  constructor (data) {
    this.observer(data)
  }
  observer (data) {
    // 要对这个data数据原有属性改写成get和set
    if (!data || typeof data !== 'object') {
      return
    }

    // 要将数据--劫持,先获取取到data的key和value
    Object.keys(data).forEach(key => {
      // 劫持
      this.defineReactive(data, key, data[key])
      this.observer(data[key])
    })
  }
  // 定义响应式
  defineReactive(obj, key, value) {
    let that = this
    let dep = new Dep() // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
    // 在获取某个值的时候做点事
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () { // 当取值是调用的方法
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set (newValue) { // 当给data属性中设置值的时候,更改获取的属性的值
        if (newValue != value) { // 这里的this不是vm
          that.observer(newValue)
          value = newValue
          dep.notify() // 通知所有人数据更新了
        }
      }
    })
  }
}

class Dep {
  constructor () {
    // 订阅的数组
    this.subs = []
  }
  // 添加订阅
  addSub (watcher) {
    this.subs.push(watcher)
  }
  // 通知
  notify () {
    this.subs.forEach(watcher => watcher.update())
  }
}

Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
img

class Compile {
  constructor (el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    if (this.el) {
      // 如果能获取到该元素,我们才开始编译
      // 1、先把这些真实的DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el)

      // 2、编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
      this.compile(fragment)

      // 3、把编译好的fragment再塞回到页面中
      this.el.appendChild(fragment)
    }
  }
  /* 专门写一些辅助方法 */
  isElementNode (node) {
    return node.nodeType === 1
  }
  // 是不是指令
  isDirective (name) {
    return name.includes('v-')
  }

  /* 核心方法 */
  compileElement (node) {
    // 带v-model
    let attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      // 判断属性名字是不是包含v-model [v, model]
      let attrName = attr.name // v-model
      if (this.isDirective(attrName)) {
        // 取到对应的值放到节点中
        let expr = attr.value
        let [, type] = attrName.split('-')
        // node this.vm.$data expr
        CompileUtil[type](node, this.vm, expr)
      }
    })
  }

  compileText (node) {
    let expr = node && node.textContent // 取文本的内容
    let reg = /\{\{([^}]+)\}\}/g // {{a}} {{b}} {{c}}
    if (reg.test(expr)) {
      // node this.vm.$data expr
      CompileUtil['text'](node, this.vm, expr)
    }
  }

  compile (fragment) {
    // 获取全部节点
    let childNodes = fragment.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isElementNode(node)) {
        // 是元素节点,还需要继续深入的检查
        // 这里需要编译元素
        this.compileElement(node)
        this.compile(node)
      } else {
        // 文本节点
        // 这里需要编译文本
        this.compileText(node)
      }
    })
  }

  node2fragment (el) { // 需要将el中的内容全部放到内存中
    // 文档碎片,不是真实的DOM,是内存中的
    let fragment = document.createDocumentFragment()
    let firstChild
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment // 内存中的节点
  }
}

CompileUtil = {
  getVal (vm, expr) { // 获取实例上对应的数据
    expr = expr.split('.')
    return expr.reduce((prev, next) => { // vm.$data.a
      return prev[next]
    }, vm.$data)
  },
  getTextVal (vm, expr) { // 获取编译文本后的结果
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      return this.getVal(vm, arguments[1])
    })
  },
  text (node, vm, expr) { // 文本处理
    let updateFn = this.updater['textUpdater']
    // message.a => [meaasge, a] vm.$data.message.a
    let value = this.getTextVal(vm, expr)
    // {{a}} {{b}} 这样的文本两个都要建立观察者
    expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      new Watcher(vm, arguments[1], (newValue) => {
        // 如果数据变化了,文本节点需要重新获取依赖的数据更新文本中的内容
        updateFn && updateFn(node, this.getTextVal(vm, expr))
      })
      updateFn && updateFn(node, value)
    })
  },
  setVal (vm, expr, value) { // [message, a]
    expr = expr.split('.')
    return expr.reduce((prev, next, currentIndex) => {
      if (currentIndex === expr.length - 1) {
        return prev[next] = value
      }
      return prev[next]
    }, vm.$data)
  },
  model (node, vm, expr) { // 输入框处理
    let updateFn = this.updater['modelUpdater']
    // 这里应该加一个监控,数据变化了应该调用watch的callback
    new Watcher(vm, expr, (newValue) => {
      // 当值变化后会调用cb将新值传递过来()
      updateFn && updateFn(node, this.getVal(vm, expr))
    })
    node.addEventListener('input', e => {
      let newValue = e.target.value
      this.setVal(vm, expr, newValue)
    })
    updateFn && updateFn(node, this.getVal(vm, expr))
  },
  updater: {
    // 文本更新
    textUpdater (node, value) {
      node.textContent = value
    },
    // 输入框更新
    modelUpdater (node, value) {
      node.value = value
    }
  }
}

Watcher

创建观察者累,目的就是给需要变化的那个元素添加一个观察者,当数据变化后执行对应的方法

class Watcher {
  constructor (vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 先获取以下老的值
    this.value = this.get()
  }
  getVal (vm, expr) { // 获取实例上对应的数据
    expr = expr.split('.')
    return expr.reduce((prev, next) => { // vm.$data.a
      return prev[next]
    }, vm.$data)
  }
  get () {
    Dep.target = this
    let value = this.getVal(this.vm, this.expr)
    Dep.target = null
    return value
  }
  // 对外暴露方法
  update () {
    let newValue = this.getVal(this.vm, this.expr)
    let oldValue = this.value
    if (newValue != oldValue) {
      this.cb(newValue) // 调用watch的callback
    }
  }
}

你可能感兴趣的:(Vue,vue)