VUE MVVM实现

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容