We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
上一篇文章我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题:
DOM
因此,没有人会将这种模板引擎用来编译动态模板。那我们如何编译动态模板呢?
回答这个问题之前,我们先要了解前端的世界何时出现了动态模板:它是由 MVVM 框架带来的,动态模板是 MVVM 框架的视图层(view)。我们知道的 MVVM 框架有 knockout.js、angular.js、avalon 和 vue。
knockout.js
angular.js
avalon
vue
对于这些框架,大部分人最熟悉的应该就是 vue,所以我下面也是以 vue 1.0 作为参考,来实现一个功能更简单的动态模板引擎。它是框架自带的一个功能,让框架能够响应数据的改变。从而刷新页面。
vue 1.0
MVVM 动态模板的特点是能最小化刷新:哪个变量改变了,与之相关的节点才会更新。这样我们就能避免上面提到的静态模板的两大问题。
要实现最小化刷新,我们要将模板中的每个绑定都收集起来。这个收集工作是框架在完成第一次渲染前就已经完成了,每个绑定都会生成一个 Directive 实例:
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)。如节点值的绑定的刷新函数是:
update
function updateTextNode() { const value = this.get() this.el.nodeValue = value console.log(this.exp + ' updated: ' + value) }
有了刷新函数,那如何做到在数据改变时调用刷新函数更新节点的值呢?我们就还要将每个指令里的相关变量都跟这个 Directive 实例关联起来。我们用一个 $binding 对象来记录,它的键是变量,值是 Binding 实例:
$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 方法的回调函数:
subs
watcher
Watcher
$watch
cb
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
首先想到的肯定是按照现有的实现来扩展,让它支持模板插值是表达式的情况。已有的实现是直接解析得到变量,那我们就继续想办法直接解析表达式得到变量。像 'age: ' + age 这种表达式直接解析出 age 其实不难。但 salary.toLocaleString() 这种就不好做了,要是 salary.toLocaleString().slice(1) 这种可以说是没办法解析了。
salary.toLocaleString()
salary.toLocaleString().slice(1)
既然这条路行不通,其实我们是有更简单的方法。既然我们都已经将 data 进行了代理,那我们就可以在 get 获取变量值时进行依赖收集。因为我们本来就会运行 Directive 实例的求值函数进行初始值的替换,这就会触发变量的 get 。具体的代码怎么写就不说了,详细的修改和支持表达式的源码。
data
get
当然现在只实现动态模板最简单的插值指令。还有一些更复杂的指令如:if 和 for 的实现方式,读者可以前往参考链接学习。
if
for
vue早期源码学习系列之四:如何实现动态数据绑定
The text was updated successfully, but these errors were encountered:
No branches or pull requests
上一篇文章我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题:
DOM
操作本身就非常大的开销,更别说每一次都替换这么大的量。DOM
绑定的事件,还会造成内存泄露。而且每一次替换都要重新绑定事件。因此,没有人会将这种模板引擎用来编译动态模板。那我们如何编译动态模板呢?
回答这个问题之前,我们先要了解前端的世界何时出现了动态模板:它是由 MVVM 框架带来的,动态模板是 MVVM 框架的视图层(view)。我们知道的 MVVM 框架有
knockout.js
、angular.js
、avalon
和vue
。对于这些框架,大部分人最熟悉的应该就是
vue
,所以我下面也是以vue 1.0
作为参考,来实现一个功能更简单的动态模板引擎。它是框架自带的一个功能,让框架能够响应数据的改变。从而刷新页面。MVVM 动态模板的特点是能最小化刷新:哪个变量改变了,与之相关的节点才会更新。这样我们就能避免上面提到的静态模板的两大问题。
要实现最小化刷新,我们要将模板中的每个绑定都收集起来。这个收集工作是框架在完成第一次渲染前就已经完成了,每个绑定都会生成一个
Directive
实例:我们知道,每个绑定都由指令和指令值(指令值可能是表达式,可能是语句,也可能就是一个变量,还可能是框架自定义的语法)构成,每种指令都有对应的刷新函数(
update
)。如节点值的绑定的刷新函数是:有了刷新函数,那如何做到在数据改变时调用刷新函数更新节点的值呢?我们就还要将每个指令里的相关变量都跟这个
Directive
实例关联起来。我们用一个$binding
对象来记录,它的键是变量,值是Binding
实例:那上面的
subs
里添加的为什么不是Directive
实例呢,而是watcher
呢?它其实是Watcher
的实例,这是为了以后能够实现$watch
方法提前引入的概念,Watcher
实例的cb
既可以是指令的刷新函数,也可以是$watch
方法的回调函数:我们先考虑最简单的情况,指令值就是一个变量,根据上面的思路,我们就可以写出最简单的实现了,代码就不贴了,有兴趣的直接看源码。
上面实现的动态模板是在我们假定了指令值是最简单的变量的情况下实现的。那要是把上面的模板改为下面这样呢?
那我们上面的实现有一些数据就不能动态刷新了,原因很简单,就是我们是直接将
'age: ' + age
和Directive
实例关联,而我们修改的只是age
,自然就找不到对应的实例了。那我们如何解决呢?首先想到的肯定是按照现有的实现来扩展,让它支持模板插值是表达式的情况。已有的实现是直接解析得到变量,那我们就继续想办法直接解析表达式得到变量。像
'age: ' + age
这种表达式直接解析出age
其实不难。但salary.toLocaleString()
这种就不好做了,要是salary.toLocaleString().slice(1)
这种可以说是没办法解析了。既然这条路行不通,其实我们是有更简单的方法。既然我们都已经将
data
进行了代理,那我们就可以在get
获取变量值时进行依赖收集。因为我们本来就会运行Directive
实例的求值函数进行初始值的替换,这就会触发变量的get
。具体的代码怎么写就不说了,详细的修改和支持表达式的源码。当然现在只实现动态模板最简单的插值指令。还有一些更复杂的指令如:
if
和for
的实现方式,读者可以前往参考链接学习。参考
vue早期源码学习系列之四:如何实现动态数据绑定
The text was updated successfully, but these errors were encountered: