上一篇文章咱们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,由于它是将模板总体编译成字符串进行全量替换。若是每次数据改变都进行一次替换,会有两个最主要的问题:前端
DOM
操做自己就很是大的开销,更别说每一次都替换这么大的量。DOM
绑定的事件,还会形成内存泄露。并且每一次替换都要从新绑定事件。所以,没有人会将这种模板引擎用来编译动态模板。那咱们如何编译动态模板呢?vue
回答这个问题以前,咱们先要了解前端的世界什么时候出现了动态模板:它是由 MVVM 框架带来的,动态模板是 MVVM 框架的视图层(view)。咱们知道的 MVVM 框架有 knockout.js
、angular.js
、avalon
和 vue
。node
对于这些框架,大部分人最熟悉的应该就是 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: ' + 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>