MVVM 中的動態數據綁定

上一篇文章咱們瞭解了怎樣實現一個簡單模板引擎。但這個模板引擎只適合靜態模板,由於它是將模板總體編譯成字符串進行全量替換。若是每次數據改變都進行一次替換,會有兩個最主要的問題:前端

  1. 性能差。DOM 操做自己就很是大的開銷,更別說每一次都替換這麼大的量。
  2. 破壞事件綁定。這個是最麻煩的,若是咱們沒有給解綁移除 DOM 綁定的事件,還會形成內存泄露。並且每一次替換都要從新綁定事件。

所以,沒有人會將這種模板引擎用來編譯動態模板。那咱們如何編譯動態模板呢?vue

回答這個問題以前,咱們先要了解前端的世界什麼時候出現了動態模板:它是由 MVVM 框架帶來的,動態模板是 MVVM 框架的視圖層(view)。咱們知道的 MVVM 框架有 knockout.jsangular.jsavalonvuenode

對於這些框架,大部分人最熟悉的應該就是 vue,因此我下面也是以 vue 1.0 做爲參考,來實現一個功能更簡單的動態模板引擎。它是框架自帶的一個功能,讓框架可以響應數據的改變。從而刷新頁面。git

MVVM 動態模板的特色是能最小化刷新:哪一個變量改變了,與之相關的節點纔會更新。這樣咱們就能避免上面提到的靜態模板的兩大問題。github

要實現最小化刷新,咱們要將模板中的每一個綁定都收集起來。這個收集工做是框架在完成第一次渲染前就已經完成了,每一個綁定都會生成一個 Directive 實例:segmentfault

class Directive {
  constructor(vm, el, exp, update) {
    this.vm = vm
    this.el = el
    this.exp = exp
    this.update = update
    this.watchers = []
    this.get = getEvaluationFn(exp).bind(this, vm.$data)

    this.bind()
  }
}

function getEvaluationFn(exp) {
  return new Function('data', 'with(data) { return ' + exp + '}')
}

咱們知道,每一個綁定都由指令和指令值(指令值多是表達式,多是語句,也可能就是一個變量,還多是框架自定義的語法)構成,每種指令都有對應的刷新函數(update)。如節點值的綁定的刷新函數是:app

function updateTextNode() {
  const value = this.get()
  this.el.nodeValue = value
  console.log(this.exp + ' updated: ' + value)
}

有了刷新函數,那如何作到在數據改變時調用刷新函數更新節點的值呢?咱們就還要將每一個指令裏的相關變量都跟這個 Directive 實例關聯起來。咱們用一個 $binding 對象來記錄,它的鍵是變量,值是 Binding 實例:框架

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

  addChild(key) {
    return this[key] || new Binding()
  }

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

那上面的 subs 裏添加的爲何不是 Directive 實例呢,而是 watcher 呢?它實際上是 Watcher 的實例,這是爲了之後可以實現 $watch 方法提早引入的概念,Watcher 實例的 cb 既能夠是指令的刷新函數,也能夠是 $watch 方法的回調函數:mvvm

class Watcher {
  constructor(vm, path, cb, ctx) {
    this.id = ++uid
    this.vm = vm
    this.path = path
    this.cb = cb
    this.ctx = ctx || vm

    this.addDep()
  }
}
class Directive {
  bind() {
    this.watchers.push(new Watcher(this.vm, this.exp, this.update, this))
  }
}

咱們先考慮最簡單的狀況,指令值就是一個變量,根據上面的思路,咱們就能夠寫出最簡單的實現了,代碼就不貼了,有興趣的直接看源碼函數

<div id="app">
  <h1>MVVM</h1>
  <p>
    <span>My name is {{name.first}}-{{name.last }},</span>{{age}} years old
  </p>
</div>
<script src="../dist/eve.js"></script>
<script>
    const app = new Eve({
      el: '#app',
      data: {
        name: {
          first: 'hugo',
          last: 'seth'
        },
        age: 1
      }
    })
    console.log(app)
</script>

圖片描述

上面實現的動態模板是在咱們假定了指令值是最簡單的變量的狀況下實現的。那要是把上面的模板改成下面這樣呢?

<h1>MVVM</h1>
  <p>
      <span>My name is {{name.first}}-{{name.last }},</span>{{'age: ' + age}} years old
  </p>
  <p>salary: {{ salary.toLocaleString() }}</p>

那咱們上面的實現有一些數據就不能動態刷新了,緣由很簡單,就是咱們是直接將 'age: ' + ageDirective 實例關聯,而咱們修改的只是 age,天然就找不到對應的實例了。那咱們如何解決呢?

首先想到的確定是按照現有的實現來擴展,讓它支持模板插值是表達式的狀況。已有的實現是直接解析獲得變量,那咱們就繼續想辦法直接解析表達式獲得變量。像 'age: ' + age 這種表達式直接解析出 age 其實不難。但 salary.toLocaleString() 這種就很差作了,要是 salary.toLocaleString().slice(1) 這種能夠說是沒辦法解析了。

既然這條路行不通,其實咱們是有更簡單的方法。既然咱們都已經將 data 進行了代理,那咱們就能夠在 get 獲取變量值時進行依賴收集。由於咱們原本就會運行 Directive 實例的求值函數進行初始值的替換,這就會觸發變量的 get 。具體的代碼怎麼寫就不說了,詳細的修改支持表達式的源碼

圖片描述

固然如今只實現動態模板最簡單的插值指令。還有一些更復雜的指令如:iffor 的實現方式,下次有機會再分享。

思考題

在最後的實現下,咱們把模板改成下面這樣(雖然不多會有人這樣寫),就會出現重複的 Watcher 實例,該如何解決這個問題?

<h1>MVVM</h1>
<p>
   hello,<span>My name is {{name.first + '-' + name.last }}</span>
</p>

參考

vue早期源碼學習系列之四:如何實現動態數據綁定

相關文章
相關標籤/搜索