手写一个简单伪MVVM模式

277 阅读2分钟

前言

我将参考下面这张图 实现一个简单的伪MVVM模式 主要针对compile, observer, watcher, dep 四个文件 图片替换文本

目录结构

  • js
    • compile.js
    • dep.js
    • observer.js
    • vue.js
    • watcher.js
  • index.html

index.html

<div id="app">
  <section>
    <input type="text" v-model="message.a">
    <div>
      {{message.a}} - {{message.a}} - {{message.b}}
    </div>
  </section>

  <section>
    <div>123</div>
    <p><span></span></p>
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </section>

  <section>
    <button v-on:click="clickMe">click me !</button>
    <h2>{{title}}</h2>
  </section>
</div>

<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/vue.js"></script>
<script>
  let vm = new Vue ({
    el: '#app',
    data: {
      message: {
        a: 'hello MVVM',
        b: 'new vue'
      },
      title: 'vue'
    },
    mounted() {
      setTimeout(() => {
        this.title = 'vue 3000'
      }, 3000)
    },
    methods: {
      clickMe() {
        console.log('clickMe ~ title', this.title)
        this.title = 'vue code'
      }
    }
  })
</script>

vue.js

class Vue {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    this.methods = options.methods

    
    if (this.$el) {
      /** 核心-数据劫持 */
      new Observer(this.$data)
      /** 核心-编译模板 */
      new Compile(this.$el, this)

      /** 辅助-数据代理到this */
      this.proxyKeys()
      /** 辅助-生命周期mounted(所有的事情处理好执行) */
      options.mounted.call(this)
    }

  }

  proxyKeys() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get() {
          return this.$data[key]
        },
        set(newValue) {
          this.$data[key] = newValue
        }
      })
    })
  }
}

compile.js

class Compile {
  constructor(el, vm) {
    this.el = this.isNodeElement(el) ? el : document.querySelector(el)
    this.vm = vm

    if (this.el) {
      let fragment = this.nodeToFragment(this.el)
      this.compile(fragment)
      this.el.appendChild(fragment)
    }
  }

  /** 辅助方法 */
  isNodeElement(node) { return node.nodeType === 1 }
  isDirective(name) { return name.includes('v-') }
  isEventDirective(name) { return name.indexOf('on:') > -1 }

  /** 核心方法 */
  /**
   * @description: 编译元素
   */
  compileElement(node) {
    let nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      if (this.isDirective(attr.name)) {
        if (this.isEventDirective(attr.name)) {
          const [, eventType] = attr.name.split(':')
          let method = attr.value
          compileUtil['event'](this.vm, node, eventType, method)
        } else { // v-model
          let expr = attr.value
          const [, type] = attr.name.split('-')
          compileUtil[type](this.vm, node, expr)
        }
      }
      
    })
  }
  /**
   * @description: 编译文本
   */
  compileText(node) {
    let textExpr = node.textContent
    if (RegText.test(textExpr)) {
      compileUtil['text'](this.vm, node, textExpr)
    }
  }
  
  /**
   * @description: 编译
   */
  compile(fragment) {
    let childNodes = fragment.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isNodeElement(node)) {
        this.compileElement(node)
        this.compile(node)
      } else {
        this.compileText(node)
      }
    })
  }

  /**
   * @description: 转为文档碎片
   */
  nodeToFragment(el) {
    let fragment = document.createDocumentFragment()
    let firstChild = el.firstChild
    while(firstChild) {
      fragment.appendChild(firstChild)
      firstChild = el.firstChild
    }
    return fragment
  }

}

const RegText = /\{\{([^}]+)\}\}/g

/** 指令工具包 */
const compileUtil = {
  /** 辅助方法 */
  getValue(vm, expr) {
    expr = expr.split('.')
    return expr.reduce((prev, next) => {
      return prev[next]
    }, vm.$data)
  },
  getTextValue(vm, textExpr) {
    return textExpr.replace(RegText, (...argments) => {
      let expr = argments[1]
      return this.getValue(vm, expr)
    })
  },
  setModelValue(vm, expr, value) {
    expr = expr.split('.')
    return expr.reduce((prev, next, currentIndex) => {
      if (currentIndex == expr.length-1) {
        prev[next] = value
      }
      return prev[next]
    }, vm.$data)
  },
  /** 核心方法 */
  text(vm, node, textExpr) {
    let textValue = this.getTextValue(vm, textExpr)
    let updaterFn = this.updater['textUpdater']
    updaterFn && updaterFn(node, textValue)
    
    textExpr.replace(RegText, (...argments) => {
      new Watcher(vm, argments[1], () => {
        updaterFn && updaterFn(node, this.getTextValue(vm, textExpr))
      })
    })
  },
  model(vm, node, expr) {
    let value = this.getValue(vm, expr)
    let updaterFn = this.updater['modelUpdater']
    updaterFn && updaterFn(node, value)

    new Watcher(vm, expr, () => {
      updaterFn && updaterFn(node, this.getValue(vm, expr))
    })

    // 监听事件
    node.addEventListener('input', e => {
      let newValue = e.target.value
      this.setModelValue(vm, expr, newValue)
    })

  },
  event(vm, node, eventType, method) {
    node.addEventListener(eventType, (e) => {
      e.preventDefault && e.preventDefault()
      let methodFn = vm.methods[method]
      methodFn && methodFn.call(vm)
    })
    
  },
  updater: {
    modelUpdater(node, value) {
      node.value = value
    },
    textUpdater(node, value) {
      node.textContent = value
    }
  }
}

dep.js

class Dep {
  constructor() {
    this.subs = []
  }

  addSub(watcher) {
    this.subs.push(watcher)
  }

  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

Dep.target = null

obsever.js

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

  observer(data) {
    if (!data || typeof data !== 'object') { return }

    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
      this.observer(data[key])
    })
  }

  defineReactive(data, key, value) {
    let self = this
    let dep = new Dep()
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newValue) {
        if (newValue != value) {
          self.observer(newValue)
          value = newValue
          dep.notify()
        }
      }
    })
  }

}

watcher.js

Dep.target = this, 这一步是神来之笔, 将watcher在compile过程中订阅, 当数据发生变化 observer 将通知变化 执行Dep的notify方法 watcher进行更新视图

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

    // 存储老值
    this.oldValue = this.getValue()
  }

  getObserverValue(vm, expr) {
    expr = expr.split('.') // [message.a]
    return expr.reduce((prev, next) => {
      return prev[next]
    }, vm.$data)
  }

  getValue() {
    Dep.target = this
    let value = this.getObserverValue(this.vm, this.expr)
    Dep.target = null
    return value
  }

  update() {
    let newValue = this.getObserverValue(this.vm, this.expr)
    if (newValue != this.oldValue) {
      this.oldValue = newValue
      this.cb()
    }
  }
}

效果图