最近在学习MVVM的实现原理,恰好在sf上看到了剖析Vue原理&实现双向绑定MVVM一文,写的很是好,摘出Vue.js中的部分源码,改造后完成了一个简单的MVVM实现。实现了双向数据绑定,我本身在学习的过程当中,也照着这篇文章中的源码从新实现了一遍。不一样之处在于,我尽可能将原来的实现写成了ES6的写法,好比使用class
代替构造函数,将observer
,dep
,watcher
,compiler
分红不一样的模块,而后使用import
,export
来互相引入,导出,最后使用rollup-babel-lib-bundler打包了一下。因此这篇文章是对上面文章的学习总结,不会写的很细。你们也能够读一下上面的文章,简单易懂。html
我从新写过的项目地址在这里,有兴趣的能够看看。node
这个简易的MVVM总共由index.js(入口文件)
,compiler.js
,dep.js
,observer.js
,watcher.js
几部分组成。git
. ├── README.md ├── dest │ ├── toy.es2015.js │ ├── toy.js │ └── toy.umd.js ├── examples │ └── index.html ├── package.json ├── rollup.config.js └── src ├── compiler.js ├── dep.js ├── index.js ├── observer.js └── watcher.js
index.js
是整个框架的入口,好比我给这个框架起了个名字叫Toy
,入口文件导出的其实就是Toy
的构造函数:github
//引入其它模块 import { observe } from './observer.js' import { Compiler } from './compiler.js' import { Watcher } from './watcher.js' //具体实现 class Toy { constructor(options){ //... } } //导出模块 export { Toy }
初始化的过程分两步:json
劫持监听全部属性,经过Object.defineProperty
将数据变成响应式的,同时在get
和set
上作一些手脚。segmentfault
编译html模板,事实上咱们在使用框架时写的html已经填充了不少框架本身的指令,语法,因此要先进行编译替换才能正确展现视图。浏览器
实现全部属性的监听就是经过Object.defineProperty
递归地定义因此属性。每个对象都会有一个对应的Observer
实例,其中的每个属性都对应有一个Dep
的实例dep
,dep
使用自增的uid
标识,做用是记录这个属性被那些订阅者(Watcher
的实例)订阅了,好在属性变化时,经过遍历dep.subs
去通知全部订阅了这个属性的watcher
去作对应的更新。babel
实现Compiler
就是对带有框架特殊API的模板进行编译,指令解析。同时将DOM与数据关联起来(实际上是经过Watcher实现的)。闭包
每一个部分负责的事情我是这样理解的:app
index.js 框架的入口,提供对外的构造函数。
observer.js 将数据变成响应式,同时经过dep
收集依赖(Watcher实例)。
dep.js 收集依赖用的,在get
中收集依赖,在set
中通知对应依赖更新。
watcher.js 数据的订阅者,一个Watcher的实例由vm
,exp
,cb
,deps
等几部分组成,vm
是对ViewModel的引用,触发get
方法将watcher
自身添加至dep
的subs
中时会用到,exp
则是当前Watcher实例监听的表达式,即数据的key
,cb
则是更新数据的回调。
当vm
的数据改变后,会触发对应的set
方法,这个属性对应的dep
会通知全部的subs
去执行自身的update
方法,而这个update
方法的内容其实只是this.cb.call(this.vm, value, oldValue)
,cb
其实是调用了updateFn
(在compiler.js
中绑定的),这时才将DOM的数据真正更新。
compiler.js 编辑DOM模板,并为每一个node节点
经过new Watcher
的方式将属性表达式exp
,updateFn(真正更新DOM的函数)
与node
关联,而后配合响应式数据就作到了view
与model
的双向绑定。
因此整个框架的运行过程是这样的:
observe
全部数据,改写了每一个数据的get和set方法,并为每一个数据关联了一个dep(经过闭包实现)。
new Compiler
开始编译模板,编译过程当中,能够提取出指令,v-text
,v-html
等,能够分析出事件函数v-click
和绑定的表达式,这时经过self.compileText(node, RegExp.$1)
,self.compile(node)
将DOM节点和表达式创建关联。
创建的关联,是DOM节点和数据表达式的关联,这一步是经过new Watcher
实现的
new Watcher
的时候,Watcher实例会将Dep.target这个全局属性指向自身,而后出发一下须要监听属性的getter,这时dep
会将Watcher实例添加到它的subs
中,Watcher实例也会标记一下这个dep已经添加过本身了,防止重复添加。这时dep
和Watcher实例已经关联起来了,数据的变化能够通知到对应的Watcher实例,Watcher实例的update方法会正确地更新DOM。
其实到这里,数据的双向绑定就已经实现了。
记录一些在学习过程当中遇到的小tips,其实都是很基础的东西。
Node.textContent: 表示一个节点及其内部节点的文本内容。以前一直都是用innerText
的,看了MDN才知道innerText
原来是IE私有的,textContent
才是标准属性。并且innerText
受样式影响,还会触发重排,因此仍是用textContent
代替吧。
Node.appendChild: 这个API有一个颇有意思的行为:若是被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除,而后再插入到新的位置.,当时我在看compiler.js
的node2Fragment
方法:
node2Fragment(el){ let fragment = document.createDocumentFragment() let child while(child = el.firstChild){ fragment.appendChild(child) } return fragment }
当时很不解为何while循环能成按照预期执行,我在浏览器屡次调用el.firstChild
拿到的也始终是第一个子节点,看了这个API的文档才发现还有这么个行为!
Node.attributes: 能够方便地获取DOM节点的属性,返回值是一个对象,其中name
是属性名,value
是属性值。
终于明白了简易MVVM框架的运做原理,也发现了一些底层API的知识,写成一些总结,这篇文章中没有贴不少代码去说实现,由于剖析Vue原理&实现双向绑定MVVM一文已经很详细了,我也是按照这个去学习的,因此我记录的是我我的的一些思想上的总结,因此可能要先看代码才能了解。分享出来,但愿能有人从中受益 :)