上一篇文章我們相識了怎樣完成一個簡樸模板引擎。但這個模板引擎只合適靜態模板,由於它是將模板團體編譯成字符串舉行全量替代。假如每次數據轉變都舉行一次替代,會有兩個最主要的題目:
- 機能差。
DOM
操縱自身就非常大的開支,更別說每一次都替代這麼大的量。 - 損壞事宜綁定。這個是最貧苦的,假如我們沒有給解綁移除
DOM
綁定的事宜,還會形成內存泄漏。而且每一次替代都要從新綁定事宜。
因而,沒有人會將這類模板引擎用來編譯動態模板。那我們怎樣編譯動態模板呢?
回覆這個題目之前,我們先要相識前端的天下什麼時候湧現了動態模板:它是由 MVVM 框架帶來的,動態模板是 MVVM 框架的視圖層(view)。我們曉得的 MVVM 框架有 knockout.js
、angular.js
、avalon
和 vue
。
關於這些框架,大部分人最熟習的應當就是 vue
,所以我下面也是以 vue 1.0
作為參考,來完成一個功用更簡樸的動態模板引擎。它是框架自帶的一個功用,讓框架可以相應數據的轉變。從而革新頁面。
MVVM 動態模板的特點是能最小化革新:哪一個變量轉變了,與之相干的節點才會更新。如許我們就可以防止上面提到的靜態模板的兩大題目。
要完成最小化革新,我們要將模板中的每一個綁建都網絡起來。這個網絡事情是框架在完成第一次襯着前就已完成了,每一個綁建都邑天生一個 Directive
實例:
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
)。如節點值的綁定的革新函數是:
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
要領的回調函數:
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: ' + age
和 Directive
實例關聯,而我們修正的只是 age
,天然就找不到對應的實例了。那我們怎樣處理呢?
起首想到的肯定是根據現有的完成來擴大,讓它支撐模板插值是表達式的狀況。已有的完成是直接剖析獲得變量,那我們就繼承想辦法直接剖析表達式獲得變量。像 'age: ' + age
這類表達式直接剖析出 age
實在不難。但 salary.toLocaleString()
這類就不好做了,如果 salary.toLocaleString().slice(1)
這類可以說是沒辦法剖析了。
既然這條路行不通,實在我們是有更簡樸的要領。既然我們都已將 data
舉行了代辦,那我們就可以在 get
獵取變量值時舉行依靠網絡。由於我們原本就會運轉 Directive
實例的求值函數舉行初始值的替代,這就會觸發變量的 get
。細緻的代碼怎樣寫就不說了,細緻的修正和支撐表達式的源碼。
固然如今只完成動態模板最簡樸的插值指令。另有一些更龐雜的指令如:if
和 for
的完成體式格局,下次有時機再分享。
思考題
在末了的完成下,我們把模板改成下面如許(雖然很少會有人如許寫),就會湧現反覆的 Watcher
實例,該怎樣處理這個題目?
<h1>MVVM</h1>
<p>
hello,<span>My name is {{name.first + '-' + name.last }}</span>
</p>