手寫一個MVVM

最近看了珠峯的架構課——實現一個MVVM。html

首先,咱們來了解一下什麼是MVVM。vue

MVVM是Model-View-ViewModel的簡寫。它本質上就是MVC 的改進版。MVVM 就是將其中的View 的狀態和行爲抽象化,讓咱們將視圖 UI 和業務邏輯分開。固然這些事 ViewModel 已經幫咱們作了,它能夠取出 Model 的數據同時幫忙處理 View 中因爲須要展現內容而涉及的業務邏輯。node

先貼一下代碼,而後再作分析。npm

<!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
        }
      })
    }
  }
}

未完待續......app

相關文章
相關標籤/搜索