Fork me on GitHub

MVVM框架进阶与实现

MVVM框架介绍

介绍

  • M :Modal 模型层
  • V :View 视图层
  • VM : ViewModal 视图模型,V和M的桥梁

MVVM框架实现了数据双向绑定

  • 当M层数据进行修改时,VM层会检测到变化,并且通知V层进行相应的修改;
  • 修改V层则会通知M层数据进行修改
  • MVVM框架实现了视图和模型层的相互解耦

几种双向数据绑定的方式

  • 发布-订阅者模式(backbone.js)
    一般通过pub、sub的方式来实现数据和视图的绑定,但是使用比较麻烦

  • 脏值检查(anjular.js)
    angular.js是通过脏值检测的方式对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变;

  • 数据劫持
    vue.js是通过数据劫持结合发布者-订阅者的方式。通过Object.defineProperty()来劫持各个属性的setter、getter,在数据发生变动的时候发布消息给订阅者,触发相应的监听回调

Vue实现思路

  • 实现一个Compile模板解析器,能够对模板中的指令和插值表达式进行解析,并且赋予不同的操作
  • 实现一个Observe数据监听器,能够对数据对象的所有属性进行监听
  • 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象链接起来,建立关系,在Observer观察对象到对象数据变化时,接受通知,同时更新DOM
  • 创建一个公共的入口对象,接受初始化的配置并且协调上面三个模块,也就是Vue

Compile实现逻辑

创建文件

  • 需要基本的html文件,vue.js文件,并且引入并且实例化vue对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<p>{{msg}}</p>
<p>{{msg}}</p>
<p v-text="msg"></p>
<p v-html="msg"></p>
</div>
</body>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
var el = new Vue({
el: '#app',
data: {
msg: "hello vue"
}
})
console.log(el)
</script>

</html>
  • 创建vue对象
1
2
3
4
5
6
7
8
9
10
11
12

class Vue {
constructor(option = {}) {
this.$el = option.el;
this.$data = option.data;

if (this.$el) {
//compile负责解析模板的内容
new Compile(this.$el, this)
}
}
}
  • 创建Compile模板解析器、
1
2
3
4
5
6
7
8
9

class Compile {
constructor(el, vm) {
//允许用户传递的dom对象可以是真实Dom或者是选择器
this.el = typeof el === 'string' ? document.querySelector(el) : el;
this.vm = vm;
}

}

创建文本碎片

DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

关于createDocumentFragment方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

if (this.el) {
let fragment = this.node2fragment(this.el)
}

/**
* 核心方法-生成文档碎片
* @param {根Dom对象} node
*/
node2fragment(node) {
let fragment = document.createDocumentFragment();
let childNodes = node.childNodes;
this.toArray(childNodes).forEach(node => {
fragment.appendChild(node);
});
}


/**
* 工具方法-类数组转化数组
* @param {类数组} likeArray
*/
toArray(likeArray) {
return [].slice.call(likeArray)
}

根据类型解析节点

  • 区分是文本节点还是元素节点来分别解析
  • 使用递归层层解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 工具方法-类数组转化数组
* @param {类数组} likeArray
*/
toArray(likeArray) {
return [].slice.call(likeArray)
}

/**
* 判断是否是元素节点
* @param {node} node
*/
isElementNode(node) {
return node.nodeType === 1;
}

解析元素节点

  • 需要分别解析v-text v-html v-modal指令
  • v-on指令因为类型原因需要特别解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 解析元素节点
* @param {节点} node
*/
compileElement(node) {
// 1.获取所有当前节点下的属性
let attributes = node.attributes;
this.toArray(attributes).forEach((attr) => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
let type = attrName.slice(2);
let attrValue = attr.value;
if (type === 'text') {
node.textContent = this.vm.$data[attrValue];
}
if (type === 'html') {
node.textContent = this.vm.$data[attrValue];
}

if (type === 'modal') {
node.value = this.vm.$data[attrValue];
}
if (this.isEventDirective(type)) {
let eventType = attrName.split(':')[1];
node.addEventListener(eventType, this.vm.$methods[attrValue].bind(this.vm))
}
}
})
}

解析文本节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

//解析文本节点
mustache(node, vm) {
let txt = node.textContent;
let reg = /\{\{(.+)\}\}/;
if (reg.test(txt)) {
let expr = RegExp.$1;
node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
}
},

//获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data;
expr.split('.').forEach(key => {
data = data[key]
})
return data;
}

Observer实现逻辑

关于数据劫持

关于Object.defineProperty方法

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
默认为 undefined。

简单案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var obj = {
name: 12,
}

var temp = obj.name;
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get() {
console.log(obj)
return temp;
},
set(newValue) {
console.log(newValue)
temp = newValue
console.log(obj)
}

})

observer实现

  • 观察者目的是使用数据劫持检测$data下的所有数据(为什么天赐还没成大佬)
1
new Observer(this.vm.$data);
  • 使用Object.keys(data)方法获取所有key,并通过Object.defineProperty为所有数据添加绑定(为什么天赐还没成大佬)

  • 为了绑定复杂数据,需要进行递归操作(为什么天赐还没成大佬)

  • 在set函数中,设置了新的数据,也需要进行监控(为什么天赐还没成大佬)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

constructor(data) {
this.walk(data);
}

walk(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
this.walk(data[key])
})
}
defineReactive(obj, key, value) {
const self = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
console.log('获取了', value)
return value;
},
set(newvalue) {
if (newvalue === value) {
return
}
console.log('设置了', newvalue)
value = newvalue;
self.walk(value)
}
})
}

Wacher实现逻辑

已经实现了页面渲染compile以及数据监听observer,接下来是将数据和页面做一个连接,即是,数据发生改变之后通知compile重新渲染,compile发生改变之后通知observer更改数据,接下来的watcher作为一个连接中心,实现这一部分的功能;

创建watcher对象

初始化需要传递三个变量

  • vm:vue对象实例
  • expr:数据对象
  • cb:callback回调函数
1
2
3
4
5
6
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldValue = this.getVMValue(vm, expr);
}

创建更新数据函数

1
2
3
4
5
6
7
8

update() {
let oldValue = this.oldValue;
let newValue = this.getVMValue(this.vm, this.expr)
if (oldValue !== newValue) {
this.cb(newValue, oldValue)
}
}

实例化watcher对象

1
2
3
4
5
6
7
8
//解析v-text指令
text(node, vm, expr) {
node.textContent = this.getVMValue(vm, expr)
window.Watcher = new Watcher(vm, expr, (newVlaue, oldValue) => {
console.log(newVlaue, '打印newValue')
node.textContent = newVlaue
})
},

获取新值调用update

1
2
3
4
5
6
7
8
9
set(newvalue) {
if (newvalue === value) {
return
}
console.log('设置了', newvalue)
value = newvalue;
window.Watcher.update();
self.walk(value)
}

使用订阅发布者模式检测数值改变

  • 声明发布者构造函数
  • 订阅检测对象
  • 实例化检测data中的每一个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Dep {
constructor() {
this.subs = [];
}

addSub(watcher) {
this.subs.push(watcher)
}

notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Dep.target = this;
this.oldValue = this.getVMValue(vm, expr);
Dep.target = null;


//observer.js

defineReactive(obj, key, value) {
const self = this;
let dep = new Dep();
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newvalue) {
if (newvalue === value) {
return
}
value = newvalue;
dep.notify();
self.walk(value)
}
})
}

实现双向数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
 node.addEventListener('input', function() {
var arr = expr.split('.');
var data = vm.$data;
arr.forEach((v, i) => {
if (i == arr.length - 1) {
data[v] = this.value;
} else {
data = data[v];
}
console.log(data);
})
})

将数据挂载到vue对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Vue {
constructor(option = {}) {
this.$el = option.el;
this.$data = option.data;
this.$methods = option.methods;
new Observer(this.$data);
this.porxy(this.$data)
this.porxy(this.$methods)
if (this.$el) {
//compile负责解析模板的内容
let c = new Compile(this.$el, this);
}
}
porxy(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return data[key]
},
set(newvalue) {
if (data[key] === newvalue) {
return
}
data[key] = newvalue;
}
})
})
}
}