MVVM原理,你看了也會vue MVVM

愛喝啤酒的雷神發表於2020-10-24

請問vue中雙向資料繫結是如何實現的?MVVM原理是什麼?

vue中的雙向資料繫結是採用資料劫持結合釋出者-訂閱者模式的方式,透過Object.defineProperty()來劫持各個屬性的setter,getter;在資料變動時釋出訊息給訂閱者,觸發相應的回撥更新函式。透過Observer來監聽model資料變化,透過Compile來解析編譯模板指令;當資料發生變化時,Observer釋出訊息給Watcher(訂閱者),訂閱者透過呼叫更新函式來更新檢視。Watcher搭起了Observer和Compile之間的通訊橋樑,從而達到資料變化 -> 檢視更新;檢視互動變化(input) -> 資料model變更的雙向繫結效果。

實現一個Compile(編譯器)

用於解析指令,並初始化檢視。並在此時建立訂閱者,繫結更新函式,在資料變化時更新檢視或資料。
class Compile{
  constructor(el, vm) {
    // 判斷el是否是一個元素節點物件
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    // 1 獲取文件碎片物件 放入記憶體中 減少頁面的迴流和重繪
    const fragement = this.node2Fragment(this.el)
    // 2 編譯模板 
    this.compile(fragement)
    // 3 追加子元素到根元素上
    this.el.appendChild(fragement)
  }
  compile(fragement) {
    // 1 獲取所有子節點
    let childNodes = fragement.childNodes
    childNodes = [...childNodes]
    childNodes.forEach(child => {
      if (this.isElementNode(child)) {
        // 是元素節點
        // 編譯元素節點
        // console.log('元素節點', child)
        this.compileElement(child)
      } else {
        // console.log('文字節點', child)
        // 編譯文字節點
        this.compileText(child)
      }
      if (child.childNodes && child.childNodes.length) {
        // 遞迴遍歷編譯所有子節點
        this.compile(child)
      }
    })
  }
  // 編譯解析元素節點
  compileElement(node){
    // 元素節點  v-html v-model v-text等指令或者事件繫結
    let attributes = node.attributes
    attributes = [...attributes]
    // 拿到所有的屬性 解析出指令
    attributes.forEach(attr => {
      const {name, value} = attr // 例如:v-text msg
      // console.log(attr, name, value)
      if (this.isDirective(name)) {
        // 判斷是否是v-開始  表示是一個指令 v-text v-html v-model v-on:click
        const [, dirctive] = name.split('-') // text html model on:click
        const [dirName, eventName] = dirctive.split(':') // dirName: text html model on eventName: click
        // 更新資料 資料驅動檢視
        compileUtil[dirName](node, value, this.vm, eventName)
        // 刪除帶有指令標籤的屬性
        node.removeAttribute('v-'+dirctive)
      } else if (this.isEventName(name)) {
        // @click="handleClick"
        let [,eventName] = name.split('@')
        compileUtil['on'](node, value, this.vm, eventName)
        // 刪除帶有@的屬性
        node.removeAttribute('@'+eventName)
      } else if (this.isBindName(name)) {
        let [, attrName] = name.split(':')
        compileUtil['bind'](node, value, this.vm, attrName)
        // 刪除帶有:的屬性
        node.removeAttribute(':'+attrName)
      }
    })
  }
  // 編譯解析文字節點
  compileText(node) {
    // {{}} 對應類似v-text
    const content = node.textContent
    if (/\{\{(.+?)\}\}/.test(content)) {
      // 正則匹配含有雙大括號的文字 並且
      // console.log(content)
      compileUtil['text'](node, content, this.vm)
    }
  }

實現一個Update(更新方法)

透過解析指令以及文字,在資料變化時,透過操作dom節點,更新檢視;修改data,更新資料。
  // 更新函式
  updater: {
    textUpdater(node, value) {
      node.textContent = value
    },
    htmlUpdater(node, value) {
      node.innerHTML = value
    },
    modelUpdater(node, value) {
      node.value = value
    },
    bindUpdater(node, attrName, value) {
      node.setAttribute(attrName, value)
    }
  }
}

實現一個Watcher(訂閱者)

資料發生變化時,呼叫回撥函式,更新檢視或資料。
class watcher{
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 先儲存舊值 用於判斷新值傳入時 是否有變化
    this.oldVal = this.getOldVal()
  }
  getOldVal() {
    Dep.target = this
    const oldVal = compileUtil.getVal(this.expr, this.vm)
    // 在呼叫getVal時會觸發observer中defineReactive中 object.defineProperty 中的get函式
    // 在get函式中拿到該watcher並新增到dep中
    Dep.target = null
    return oldVal
  }
  update() {
    const newVal = compileUtil.getVal(this.expr, this.vm)
    if (this.oldVal !== newVal) {
      this.oldVal = newVal
      this.cb(newVal)
    }
  }
}
編譯模板指令時,初始化一個watcher例項並繫結更新函式
 text(node, expr, vm) {
    let value
    if (expr.indexOf('{{') !== -1) {
      // 處理 存在雙大括號的文字 {{personalbar.name}} {{msg}}
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // replace 替換回撥函式引數分別有:0 匹配到的字串 1在使用組匹配 組匹配到的值 匹配值在原字串中的索引 原字串 
        // 繫結觀察者 將來資料發生變化 觸發這裡的回撥 進行更新
        new watcher(vm, args[1], (newVal) => {
          // console.log('newVal', newVal, this.getContentVal(expr, vm))
          // 在此有個疑問 newVal和getContentVal重新解析原表示式獲取的值是一樣的 不知作者為啥要重新解析一遍?
          this.updater.textUpdater(node, this.getContentVal(expr, vm))
        })
        return this.getVal(args[1], vm)
      })
    } else {
      // 處理v-text expr: msg vm: 整個例項
      new watcher(vm, expr, (newVal) => {
        this.updater.textUpdater(node, newVal)
      })
      value = this.getVal(expr, vm)
    }
    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)
    // 建立監聽者 並透過watcher中的update來繫結回撥這個更新函式  資料 =》 檢視
    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]
    // 修改函式this指向為當前vue例項
    node.addEventListener(eventName, fn.bind(vm), false)
  },
  bind(node, expr, vm, attrName) {
    const value = this.getVal(expr, vm)
    this.updater.bindUpdater(node, attrName, value)
  },



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984138/viewspace-2729394/,如需轉載,請註明出處,否則將追究法律責任。

相關文章