我对MVVM的学习笔记

前言

最近在学习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

  1. 劫持监听全部属性,经过Object.defineProperty将数据变成响应式的,同时在getset上作一些手脚。segmentfault

  2. 编译html模板,事实上咱们在使用框架时写的html已经填充了不少框架本身的指令,语法,因此要先进行编译替换才能正确展现视图。浏览器

实现全部属性的监听就是经过Object.defineProperty递归地定义因此属性。每个对象都会有一个对应的Observer实例,其中的每个属性都对应有一个Dep的实例depdep使用自增的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自身添加至depsubs中时会用到,exp则是当前Watcher实例监听的表达式,即数据的keycb则是更新数据的回调。
    vm的数据改变后,会触发对应的set方法,这个属性对应的dep会通知全部的subs去执行自身的update方法,而这个update方法的内容其实只是this.cb.call(this.vm, value, oldValue)cb其实是调用了updateFn(在compiler.js中绑定的),这时才将DOM的数据真正更新。

  • compiler.js 编辑DOM模板,并为每一个node节点经过new Watcher的方式将属性表达式expupdateFn(真正更新DOM的函数)node关联,而后配合响应式数据就作到了viewmodel的双向绑定。

因此整个框架的运行过程是这样的:

  1. observe全部数据,改写了每一个数据的get和set方法,并为每一个数据关联了一个dep(经过闭包实现)。

  2. new Compiler开始编译模板,编译过程当中,能够提取出指令,v-text,v-html等,能够分析出事件函数v-click和绑定的表达式,这时经过self.compileText(node, RegExp.$1),self.compile(node)将DOM节点和表达式创建关联。

  3. 创建的关联,是DOM节点和数据表达式的关联,这一步是经过new Watcher实现的

  4. 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.jsnode2Fragment方法:

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一文已经很详细了,我也是按照这个去学习的,因此我记录的是我我的的一些思想上的总结,因此可能要先看代码才能了解。分享出来,但愿能有人从中受益 :)

相关文章
相关标签/搜索