手写一个MVVM

最近看了珠峰的架构课——实现一个MVVM。

首先,我们来了解一下什么是MVVM。

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

先贴一下代码,然后再做分析。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>myMVVM</title>
</head>
<body>
  <div id="root">
    <input type="text" v-model='person.name' >
    <h1>{{message}}</h1>
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
    {{person.age}}
    <br />
    <button v-on:click="change">点我</button>
    <br />
    {{getNewName}}
    <div v-html="message"></div>
  </div>
  <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
  <script src="Vue.js"></script>
  <script>
      let vm = new Vue({
        el: '#root',
        data: {
          message: 'she is a bad girl',
          person: {
            age: 20,
            name: 'zhangsan'
          }
        },
        computed: {
          getNewName() {
            return this.person.name + 'hahaha'
          }
        },
        methods: {
          change() {
            console.log("11111")
          }
        }
        
      })
  </script>
</body>
</html>

MVVM的简易实现(还有很多功能没有涉及到)

/**
 * 订阅发布 调度中心
 */
class Dep {
  constructor() {
    this.subs = [] // 存放所有的watcher
  }
  // 添加watcher, 订阅
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 发布
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

/**
 *  观察者模式 观察者,被观察者
 */
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb

    // 默认先存放旧值
    this.oldValue = this.get(vm, expr)
  }
  // 获取旧的值
  get() {
    Dep.target = this
    let value = CompileUtil.getVal(this.vm, this.expr)
    Dep.target = null
    return value
  }
  // 数据更新
  update() {
    let newVal = CompileUtil.getVal(this.vm, this.expr)
    if(newVal != this.oldValue) {
      this.cb(newVal)
    }
  }
}

/**
 * 实现数据劫持
 */
class Observer {
  constructor(data) {
    this.observe(data)
  }
  observe(data) {
    if(data && typeof data == 'object') {
      for(let key in data) {
        this.defineReactive(data, key, data[key])
      }
    }
  }

  defineReactive(obj, key, value) {
    this.observe(value)
    let dep = new Dep() // 给每一个属性都加上一个具有发布订阅的功能
    Object.defineProperty(obj, key, {
      get() {
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set: (newValue) => {
        if(newValue != value) {
          this.observe(newValue)
          value = newValue
          dep.notify()
        }
      }
    })
  }
}

/**
 * 编译模板
 */
class Compiler {
  constructor(el, vm) {
    this.vm = vm
    // 判断el是否是个元素,如果不是就获取它
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    // console.log(this.el)
    // 把当前节点中的元素放到内存中
    let fragment = this.node2fragment(this.el)
    
    // 把节点中的内容进行替换

    // 编译模板 用数据来编译
    this.compile(fragment)

    // 把内容再塞回页面中
    this.el.appendChild(fragment)
  }

  // 是否是指令 v-开头的
  isDirective(attrName) {
    // startsWith('v-') 或 split('-')
    return attrName.indexOf('v-') !== -1
  }
  // 编译元素
  compileElement(node) {
    let attributes = node.attributes
    // [...attributes] 或 Array.from 等价 Array.prototype.slice.call
    Array.from(attributes).forEach(attr => {
      let { name, value: expr } = attr
      if(this.isDirective(name)) {
        // 
        let [, directive] = name.split('-')
        // console.log(name,node, expr, directive)
        if(directive.indexOf(':' !== -1)) {
          let [directiveName, eventName] = directive.split(':')
          CompileUtil[directiveName](node, expr, this.vm, eventName)
        } else {
          CompileUtil[directive](node, expr, this.vm)
        }
        
      }
    })
  }
  // 编译文本 找{{}}
  compileText(node) {
    let content = node.textContent
    if(/\{\{(.+?)\}\}/.test(content)) {
      // console.log(content) // 找到所有文本
      CompileUtil['text'](node, content, this.vm)
    }
  }

  // 编译内存中的dom节点 核心的编译方法
  compile(node) {
    let childNodes = node.childNodes
    Array.from(childNodes).forEach(child => {
      if(this.isElementNode(child)) {
        this.compileElement(child)
        // 如果是元素的话,需要除去自己,再去遍历子节点
        this.compile(child)
      } else {
        this.compileText(child)
      }
    })
  }
  // 把节点移动到内存中 appendChild方法
  node2fragment(node) {
    let fragment = document.createDocumentFragment()
    let firstChild
    while(firstChild = node.firstChild) {
      fragment.appendChild(firstChild)
    }

    return fragment
  }
  // 判断是否为元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
}

/**
 * 编译工具函数
 */
CompileUtil = {
  // 根据表达式取对应的数据
  getVal(vm, expr) {
    return expr.split('.').reduce((data, current) => {
      return data[current]
    }, vm.$data)
  },
  getContentVal(vm, expr) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(vm, args[1])
    })
  },
  setVal(vm, expr, value) {
    expr.split('.').reduce((data, current, index, arr) => {
      if(index === arr.length - 1) {
        return data[current] = value
      }
      return data[current]
    }, vm.$data)
  },
  // 解析v-model指令
  model(node, expr, vm) {
    // node.value
    let fn = this.updater['modelUpdater']
    new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
      fn(node, newVal)
    })
    node.addEventListener('input', e => {
      let value = e.target.value
      this.setVal(vm, expr, value)
    })
    let value = this.getVal(vm, expr)
    fn(node, value)
  },
  on(node, expr, vm, eventName) {
    node.addEventListener(eventName, e => {
      vm[expr].call(vm, e)
    })
  },
  text(node, expr, vm) {
    let fn = this.updater['textUpdater']
    let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      // 给表达式每个变量都加上观察者
      new Watcher(vm, args[1], () => {
        fn(node, this.getContentVal(vm, expr)) // 返回一个全的字符串
      })
      return this.getVal(vm, args[1])
    })
    fn(node, content)
  },
  html(node, expr, vm) {
    // node.innerHTML
    let fn = this.updater['htmlUpdater']
    new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
      fn(node, newVal)
    })
    let value = this.getVal(vm, expr)
    fn(node, value)

  },
  updater: {
    modelUpdater(node, value) {
      node.value = value
    },
    textUpdater(node, value) {
      node.textContent = value
    },
    htmlUpdater(node, value) {
      node.innerHTML = value
    }
  }
}

/**
 * Vue构造函数
 */
class Vue {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data

    let computed = options.computed
    let methods = options.methods

    if(this.$el) {
      //  做数据劫持
      new Observer(this.$data)
      // console.log(this.$data)

      for(let key in computed) { // 依赖关系
        Object.defineProperty(this.$data, key, {
          get: () => {
            return computed[key].call(this)
          }
        })
      }

      for(let key in methods) {
        Object.defineProperty(this, key, {
          get() {
            return methods[key]
          }
        })
      }

      // vm上的取值操作都代理上vm.$data上
      this.proxyVm(this.$data)

      // 编译模板
      new Compiler(this.$el, this)
    }
  }
  // 代理
  proxyVm(data) {
    for(let key in data) {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(newVal) {
          data[key] = newVal
        }
      })
    }
  }
}

未完待续......

posted @ 2019-07-29 14:31  一江西流  阅读(721)  评论(0编辑  收藏  举报