美文网首页Vue
VUE MVVM实现

VUE MVVM实现

作者: bloom_os | 来源:发表于2020-03-31 21:29 被阅读0次

    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中的回调,实现更新

    相关文章

      网友评论

        本文标题:VUE MVVM实现

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