Vue

VUE MVVM实现

2020-03-31  本文已影响0人  bloom_os

VUE MVVM实现

详细代码请参考: https://github.com/osorso/VUE_MVVM

理解MVVM

mvvm - Model View ViewModel 数据 视图 视图模型

其中Model ---> data, View ---> template, ViewModel ---> new Vue({...})

view通过绑定事件的方式将model联系在一起, model可以通过数据绑定的形式影响view, 而这两种联系则是通过viewModel实现的

mvvm.png

实现原理

创建html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue响应式实现</title>
  </head>

  <body>
    <div id="app">
      <h1>{{ person.name }} ----- {{ person.age }}</h1>
      <h2>{{ person.fav }}</h2>
      <ul>
        <li>123</li>
        <li>123</li>
        <li>123</li>
      </ul>
      <h3>{{ msg }}</h3>
      <div v-text="msg"></div>
      <div v-html="htmlStr"></div>
      <input type="text" v-model="msg" />
      <button @click="handleClick">点击切换名称</button>
    </div>
  </body>
  <script src="Observer.js"></script>
  <script src="Mvue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: '这是一条提示信息',
        htmlStr: '<h5>这是一个h5类型的数据</h5>',
        person: {
          name: '小明',
          age: 16,
          fav: '玩游戏'
        }
      },
      methods: {
        handleClick() {
          const arr = ['小米', '小明', '校长', '学生', '小刘', '红色', '黄色', '白色', '黑色', '紫色', '蓝色', '绿色', '小花', '王二', '张三', '李四', '韩梅梅', '李雷']
          const index = Math.ceil(Math.random() * (arr.length - 1))
          this.person.name = arr[index]
        }
      }
    })
  </script>
</html>

创建一个VUE实例类

class Vue{
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    this.$options = options
    if (this.$el) {
      new Observer(this.$data)
      new Compile(this.$el, this)
    }
  }
}

通过创建vue实例,将el,data, options等绑定在这个实例上面, 同时当el存在时, 创建数据观察者Observe以及指令解析器Compile

实现compile(指令解析器)

class Compile{
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    const fragment = this.nodeFragment(this.el)
    this.compile(fragment)
    this.el.appendChild(fragment) // 追加子元素到根元素
  }
}

​ fragment此方法为获取文档碎片对象,将其放入内存中,减少回流重绘

nodeFragment(el) {
    // 创建文档碎片
    const f = document.createDocumentFragment()
    let firstChild;
    while (firstChild = el.firstChild) {
        f.appendChild(firstChild)
    }
    return f
}

​ compile(编译模版)

compile(fragment) {
    // 获取子节点
    const childNodes = fragment.childNodes;
    [...childNodes].forEach(child => {
        if (this.isElementNode(child)) {
            this.compileElement(child)
        } else {
            this.compileText(child)
        }
        if (child.childNodes && child.childNodes.length) {
            this.compile(child)
        }
    })
}

compileElement(node) {
    const attributes = node.attributes
    ;[...attributes].forEach(attr => {
        const { name, value } = attr
        if (this.isDirective(name)) { // 获取到指令v-html v-text等
            const [,dirctive] = name.split('-') // text, html, model on:click等
            const [dirName, eventName] = dirctive.split(':') // text html model on
            // 更新数据, 数据驱动视图
            compileUtil[dirName](node, value, this.vm, eventName)
            // 删除有指令的标签上的属性
            node.removeAttribute('v-' + dirctive)
        } else if(this.isEventName(name)) {
            let [,eventName] = name.split('@')
            compileUtil['on'](node, value, this.vm, eventName)
        }
    })
}

compileText(node) {
    const content = node.textContent
    if (/\{\{(.+?)\}\}/.test(content)) {
        compileUtil['text'](node, content, this.vm)
    }
}

isEventName(attrName) {
    return attrName.startsWith('@')
}

nodeFragment(el) {
    // 创建文档碎片
    const f = document.createDocumentFragment()
    let firstChild;
    while (firstChild = el.firstChild) {
        f.appendChild(firstChild)
    }
    return f
}

isDirective(attrName) {
    return attrName.startsWith('v-')
}

isElementNode(node) {
    return node.nodeType === 1
}

compile中解析模板的方法

const compileUtil = {
  getVal(expr, vm) { // 获取div v-text="person.name"></div> 中的person.name这个属性--使得可以在$data顺利取得此值
    return expr.split('.').reduce((data, currentVal) => {
      currentVal = currentVal.trim()
      return data[currentVal];
    }, vm.$data)
  },

  setVal(expr, vm, inputVal) {
    return expr.split('.').reduce((data, currentVal) => {
      currentVal = currentVal.trim()
      data[currentVal] = inputVal
    }, vm.$data)
  },

  getContentVal(expr, vm) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(args[1], vm)
    })
  },

  text(node, expr, vm) { // expr---msg
    let value;
    if (expr.indexOf('{{') !== -1) {
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // 绑定观察者,数据发生变化时触发 进行更新
        new Watcher(vm, args[1], () => {
          this.updater.textUpdater(node, this.getContentVal(expr, vm))
        })
        return this.getVal(args[1], vm)
      })
    } else {
      value = this.getVal(expr, vm)
      new Watcher(vm, expr, (newVal) => {
        this.updater.textUpdater(node, newVal)
      })
    }
    this.updater.textUpdater(node, value)
  },

  html(node, expr, vm) {
    const value = this.getVal(expr, vm)
    new Watcher(vm, expr, (newVal) => {
      this.updater.htmlUpdater(node, newVal)
    })
    this.updater.htmlUpdater(node, value)
  },
  // 实现双向数据绑定
  model(node, expr, vm) {
    const value = this.getVal(expr, vm)
    // 绑定更新函数 数据=> 视图
    new Watcher(vm, expr, (newVal) => {
      this.updater.modelUpdater(node, newVal)
    })
    // 视图 => 数据 => 视图
    node.addEventListener('input', (e) => {
      this.setVal(expr, vm, e.target.value)
    })
    this.updater.modelUpdater(node, value)
  },

  on(node, expr, vm, eventName) {
    let fn = vm.$options.methods && vm.$options.methods[expr]
    node.addEventListener(eventName, fn.bind(vm), false)
  },

  bind(node, expr, vm, attr) {},

  // 更新的函数
  updater: {
    textUpdater(node, value) {
      node.textContent = value
    },

    htmlUpdater(node, value) {
      node.innerHTML = value
    },

    modelUpdater(node, value) {
      node.value = value
    }
  }
}

实现Observe(数据劫持)

class Observer{
  constructor(data) {
    this.observer(data)
  }

  observer(data) {
    if (data && typeof data === 'object') {
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key])
      })
    }
  }

  defineReactive(obj, key, value) {
    // 递归遍历
    this.observer(value)
    const dep = new Dep()
    // 劫持并监听所有属性
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      get() {
        // 订阅数据变化时,往Dep中添加观察者
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set: newVal => {
        this.observer(value) // 如果设置新值时需重新劫持新值,确保不会因为改变值而导致未能劫持数据
        if (newVal !== value) {
          value = newVal
        }
        // 通知变化
        dep.notify()
      }
    })
  }
}

observe通过对数据对象的递归遍历, 结合Object.defineProperty()从而实现劫持各个属性的setter,getter, 从而实现当属性对应的数据变化时,发布消息给订阅者, 通知其变化!

订阅者实现

class Dep{
  constructor() {
    this.sub = []
  }
  // 收集观察者
  addSub(watcher) {
    this.sub.push(watcher)
  }
  // 通知观察者更新
  notify() {
    this.sub.forEach(w => w.update())
  }
}

订阅者的功能主要是收集变化并通知观察者更新,架起了Observe与Watcher之间的桥梁

Watcher(监听器实现)

class Watcher{
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.oldVal = this.getOldVal()
  }

  getOldVal() {
    Dep.target = this // 将watcher挂在到dep上
    const oldVal = compileUtil.getVal(this.expr, this.vm)
    Dep.target = null // 获取到值后清除所挂载的watcher
    return oldVal
  }

  update() {
    const newVal = compileUtil.getVal(this.expr, this.vm)
    if (newVal !== this.oldVal) {
      this.cb(newVal)
    }
  }
}

通过Watcher,则可实现Observe与Compile之间的联系,从而实现数据变化驱动视图

实现原理: 往订阅者中添加Watcher,与Observe建立联系, 而Watcher自身有个update()方法,此方法与Compile建立联系,当属性变化时, Observe则会通知Watcher,从而Watcher调用update()方法, 触发Compile中的回调,实现更新

上一篇下一篇

猜你喜欢

热点阅读