MVVM框架介绍
-
M (Model,数据模型层)
-
V (View,视图层,数据展示,html页面)
-
VM (ViewModel,视图模型,V与M连接的桥梁)
-
MVVM框架实现了数据的双向绑定
- 当M层数据进行修改时,VM层会检测到变化,并且通知V层进行相应得修改
- 修改V层则会通知M层数据进行修改
- MVVM框架实现了视图与模型层得相互解耦
几种双向数据绑定的方式
- 发布-订阅者模式,也叫观察者模式(backbone.js)
- 它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
- 一般通过pub、sub的方式来实现数据和视图的绑定,但是用起来比较麻烦举例: 微信公众号
订阅者: 只需要订阅微信公众号
发布者(公众号): 发布新文章的时候,推送给所有订阅者
优点:- 解耦合
- 订阅者不用每次去查看公众号是否有新的文章
- 发布者不用关心谁订阅了它,只要给所有订阅者推送即可
- 脏值检查(angular.js)
- angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变 - 数据劫持
- vue.js 则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
- Object.defineProperty不兼容IE8及以下的版本,所以vuejs不兼容ie8
实现双向绑定需要三个模块
首先建立vue实例,此页代码仅演示文本节点的双向绑定
class Vue {
constructor(option = {}) {
this.$el = option.el
this.$data = option.data
new Observer(this.$data) //数据劫持用,单纯渲染页面不用此行代码
this.proxy(this.$data) // 最后第4步看此行代码
if (this.$el) new Compile(this.$el, this)
}
proxy(data) { // 最后第4步看此方法
Object.keys(data).forEach(key => {
console.log(this)
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (data[key] == newValue) {
return
}
data[key] = newValue
}
})
})
}
}
- Compile
编译模块,实现data数据渲染至视图上,methods方法可以触发
解读:
(1)创建Vue构造函数,添加el,data,methods方法;
(2)获取el DOM对象下的全部子节点,放入内存中, 放入内存的dom节点为fragment碎片,但不存在与dom树中,由document.createDocumentFragment()创建,遍历fragment存放的节点,利用正则等表达式匹配并替换data数据,重新更新fragment的dom元素,并一起放进真实的dom树。这样做仅一次 重绘回流 即可渲染页面,大大减少了浏览器的负载,优化性能。代码如下
//编译模块
class Compile {
constructor(el, vm) {
this.el = typeof el === 'string'? document.getElementById('app'): el
this.vm = vm
if (this.el) {
let fragment = this.nodeTfragment(this.el) //把所有节点放进fragment
this.compile(fragment)
this.el.appendChild(fragment)
}
}
nodeTfragment(node) {
let fragment = document.createDocumentFragment() //建立文档碎片
let childNodes = [].slice.call(node.childNodes) //[].slice.call 类数组用这一行代码转换成数组
childNodes.forEach(element => {
fragment.appendChild(element)
});
return fragment
}
compile(fragment) {
let childNodes = [].slice.call(fragment.childNodes)
childNodes.forEach( node => {
if (node.nodeType === 3) { //如果是文本节点
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1 //取正则的第一组,也就是第一个出现的一对括号
node.textContent = txt.replace(reg, this.vm.$data[expr])
new Watch (this.vm, expr, newValue => { //对每一个数据订阅 添加观察者
node.textContent = txt.replace(reg, newValue)
})
}
}
})
}
}
- Observer
劫持数据模块劫持数据模块
解读:
遍历数据添加劫持方法
//数据劫持
class Observer {
constructor(data) {
this.data = data
this.walk(data) //核心方法
}
walk(data) {
if (!data || typeof data != 'object') return
Object.keys(data).forEach(key => {
let dep = new Dep() //此行代码请参考第3步
let value = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target) //此行代码请参考第3步
return value
},
set(newValue) { //每当数据改变都要执行此方法
if (value === newValue) return
value = newValue
dep.notify() //此行代码请参考第3步,通知订阅者要改变
}
})
})
}
}
- Watcher 用于连接 1与2 的模块
在Compile模块为每个数据添加watcher构造函数(为订阅者模式),把各个watcher实例添加进一个数组集合,该数组称为Dep;至此,数据劫持与发布-订阅者模式结合,发布的意思为更改数据;
即数据改变则拿到了所有的watcher,通过回调函数改变数据,数据改变后则重新渲染页面。
//观察者(订阅者)
class Watch {
constructor(vm, expr, callback) {
this.vm = vm
this.expr = expr
this.callback = callback
Dep.target = this
this.olderValue = vm.$data[expr] //每当获取data数据时,都要执行Observer 模块Object.defineProperty的get方法
Dep.target = null
}
updata() {
let oldValue = this.olderValue
let newValue = vm.$data[this.expr]
if (oldValue != newValue) {
this.callback(newValue)
}
}
}
//订阅者的操作
class Dep {
constructor() {
this.sub = []
}
addSub(watcher) {
this.sub.push(watcher)
}
notify() {
this.sub.forEach ((sub) => {
sub.updata()
})
}
}
- 将所有data的方法直接挂在至vm实例
最后,通过proxy方法,利用Object.defineProperty方法依次添加至vm实例上请看第一段代码
- 结合下图总结
(1) 绿色线 :实例化Vue后,通过Compile去解析所有的指令,能看到页面渲染的内容
(2)红色线:每解析一个指令,实例一个watcher,订阅数据的变化,逐个添加进Dep 构造函数的数组中
(3)蓝色线: 再通过Observer 劫持到了数据变化,通过Dep拿到了所有的订阅者,即Watcher,通知Watcher数据发生了改变,进而触发每个Watcher的update方法,更新视图。
此文档有些得不明白的地方请加v: zaq1312135000