摘要:要实现最小化刷新,我们要将模板中的每个绑定都收集起来。思考题在最后的实现下,我们把模板改为下面这样虽然很少会有人这样写,就会出现重复的实例,该如何解决这个问题,参考早期源码学习系列之四如何实现动态数据绑定
上一篇文章我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题:
性能差。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)) } }
我们先考虑最简单的情况,指令值就是一个变量,根据上面的思路,我们就可以写出最简单的实现了,代码就不贴了,有兴趣的直接看源码。
MVVM
My name is {{name.first}}-{{name.last }},{{age}} years old
上面实现的动态模板是在我们假定了指令值是最简单的变量的情况下实现的。那要是把上面的模板改为下面这样呢?
MVVM
My name is {{name.first}}-{{name.last }},{{"age: " + age}} years old
salary: {{ salary.toLocaleString() }}
那我们上面的实现有一些数据就不能动态刷新了,原因很简单,就是我们是直接将 "age: " + age 和 Directive 实例关联,而我们修改的只是 age,自然就找不到对应的实例了。那我们如何解决呢?
首先想到的肯定是按照现有的实现来扩展,让它支持模板插值是表达式的情况。已有的实现是直接解析得到变量,那我们就继续想办法直接解析表达式得到变量。像 "age: " + age 这种表达式直接解析出 age 其实不难。但 salary.toLocaleString() 这种就不好做了,要是 salary.toLocaleString().slice(1) 这种可以说是没办法解析了。
既然这条路行不通,其实我们是有更简单的方法。既然我们都已经将 data 进行了代理,那我们就可以在 get 获取变量值时进行依赖收集。因为我们本来就会运行 Directive 实例的求值函数进行初始值的替换,这就会触发变量的 get 。具体的代码怎么写就不说了,详细的修改和支持表达式的源码。
当然现在只实现动态模板最简单的插值指令。还有一些更复杂的指令如:if 和 for 的实现方式,下次有机会再分享。
思考题在最后的实现下,我们把模板改为下面这样(虽然很少会有人这样写),就会出现重复的 Watcher 实例,该如何解决这个问题?
参考MVVM
hello,My name is {{name.first + "-" + name.last }}
vue早期源码学习系列之四:如何实现动态数据绑定
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/107754.html
摘要:看这篇之前,如果没有看过之前的文章,移步拉到文章末尾查看之前的文章。而该组件实例的父实例却并不固定,所以我们将这些在使用时才能确定的参数在组件实例化的时候传入。系列文章地址优化优化总结 看这篇之前,如果没有看过之前的文章,移步拉到文章末尾查看之前的文章。 前言 在上一步,我们实现 extend 方法,用于扩展 Vue 类,而我们知道子组件需要通过 extend 方法来实现,我们从测试例...
摘要:面向对象是自己组装电脑,硬件已生产完毕。面向过程吃狗屎面向对象狗吃屎确切的讲是一种软件设计规范,早在年的理念就已经诞生。后期的维护成本会减少很多。减轻了开发人员的负担,也减少了操作逻辑导致业务逻辑混乱的可能性。 什么是MVC,什么是MVVM? 面向过程 --> 面向对象 --> MVC --> MV* 面向过程: 开发人员按照需求逻辑顺序开发代码逻辑,主要思维模式在于如何实现。先细节,...
摘要:接下来要看看这个订阅者的具体实现了实现订阅者作为和之间通信的桥梁,主要做的事情是在自身实例化时往属性订阅器里面添加自己自身必须有一个方法待属性变动通知时,能调用自身的方法,并触发中绑定的回调,则功成身退。 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,...
阅读 3069·2023-04-25 16:50
阅读 903·2021-11-25 09:43
阅读 3512·2021-09-26 10:11
阅读 2517·2019-08-26 13:28
阅读 2529·2019-08-26 13:23
阅读 2418·2019-08-26 11:53
阅读 3564·2019-08-23 18:19
阅读 2986·2019-08-23 16:27