自己动手写MVVM(二):实现Compile

在上一个章节,我们实现了Observe,同时提及Observe的调用是依赖Watcher的。

但是我们再回头看MVVM框架的实现:

upload successful

Watcher的订阅是依赖Compile的,所以我们先实现Compile再去实现Watcher也是可以的。

实现Compile

实现思路

Compile的实现大概思路是如下:

  • 解析指令,将指令中的变量替换为数据,并初始化视图
  • 订阅数据更新,绑定视图与数据更新的函数
  • 接收到数据后,对视图进行update操作

在实现Compile前,我们需要先了解Document.createDocumentFragment()方法。该方法指向空DocumentFragment对象的引用,这不是主DOM树的一部分,用于创建文档片段并附加到DOM数中。

upload successful

为什么需要使用fragment

在对节点进行遍历解析时,会多次操作dom节点,为了提高性能和减少回流,会将Vue实例的根节点转换为fragment(文档碎片),在解析编译完成后,再将fragment添加到主dom树中。

DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树的重新渲染的操作(reflow) ,且不会导致性能等问题。

具体实现方法

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// 实现Compile
function Compile(el, vm) {
// 保存vm
this.$vm = vm;
// 保存el元素
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
// 如果el元素存在
if (this.$el) {
// 1. 取出el中所有子节点, 封装在一个framgment对象中
this.$fragment = this.node2Fragment( this.$el );
// 2. 编译fragment中所有层次子节点
this.init();
// 3. 将fragment添加到el中
this.$el.appendChild(this.$fragment);
}
}

Compile.prototype = {
// 初始化Compile
init(){
this.compileElement( this.$fragment );
},

// 将dom转换为Fragment,documentFragment将dom进行批量处理
node2Fragment: function( el ) {
// 创建一个空的fragment
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},

// 处理节点
compileElement: function(el) {
// 得到所有子节点
var childNodes = el.childNodes,
// 保存compile对象
me = this;
// 将类数组转换为数组,并遍历所有子节点
[].slice.call(childNodes).forEach(function(node) {
// 获得节点的文本内容
var text = node.textContent;
// 匹配大括号表达式
var reg = /\{\{(.*)\}\}/; // {{name}}
// 如果是元素节点
if( me.isElementNode(node) ) {
// 编译元素节点的指令属性
me.compile(node);
// 如果是一个大括号表达式格式的文本节点
} else if (me.isTextNode(node) && reg.test(text)) {
// 编译大括号表达式格式的文本节点
me.compileText(node, RegExp.$1);
}
// 如果子节点还有子节点
if (node.childNodes && node.childNodes.length) {
// 递归编译
me.compileElement(node);
}
});
},

// 编译元素节点的指令属性
compile: function(node) {
// 得到所有标签属性节点
var nodeAttrs = node.attributes,
me = this;
// 遍历所有属性
[].slice.call(nodeAttrs).forEach(function(attr) {
// 得到属性名: v-on:click
var attrName = attr.name;
// 判断是否是指令属性
if (me.isDirective(attrName)) {
// 得到表达式(属性值): test
var exp = attr.value;
// 得到指令名: on:click
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
// 解析事件指令
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
// 解析普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}

// 移除指令属性
node.removeAttribute(attrName);
}
});
},

// 编译大括号表达式格式的文本节点
compileText: function(node, exp) {
// 调用编译工具对象解析
compileUtil.text(node, this.$vm, exp);
},

isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},

isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
},

isElementNode: function(node) {
return node.nodeType == 1;
},

isTextNode: function(node) {
return node.nodeType == 3;
}
}

// 指令处理集合
var compileUtil = {
// 解析: v-text/{{}}
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// 解析: v-html
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},

// 解析: v-model
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');

var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}

me._setVMVal(vm, exp, newValue);
val = newValue;
});
},

// 解析: v-class
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},

// 真正用于解析指令的方法
bind: function(node, vm, exp, dir) {
/*实现初始化显示*/
// 根据指令名(text)得到对应的更新节点函数
// 取到一个object的属性,有2个方法,一个是obj. 一个是obj[]
// 当我们要取得属性是一个变量的时候,使用obj[]
var updaterFn = updater[dir + 'Updater'];
// 如果存在调用来更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));

// 创建表达式对应的watcher对象
new Watcher(vm, exp, function(value, oldValue) { /*更新界面*/
// 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
updaterFn && updaterFn(node, value, oldValue);
});
},

// 事件处理
eventHandler: function(node, vm, exp, dir) {
// 得到事件名/类型: click
var eventType = dir.split(':')[1],
// 根据表达式得到事件处理函数(从methods中): test(){}
fn = vm.$options.methods && vm.$options.methods[exp];
// 如果都存在
if (eventType && fn) {
// 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm
node.addEventListener(eventType, fn.bind(vm), false);
}
},

// 得到表达式对应的value
_getVMVal: function(vm, exp) {
// 这里为什么要forEach呢?
// 如果你的exp是a.b.c.c.d呢 就需要forEach 如果只是一层 当然不需要遍历啦
var val = vm._data;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},

_setVMVal: function(vm, exp, value) {
var val = vm._data;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};

// 包含多个用于更新节点方法的对象
var updater = {
// 更新节点的textContent
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},

// 更新节点的innerHTML
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},

// 更新节点的className
classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},

// 更新节点的value
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};

上述代码功能如下:

  • 通过递归遍历,保证每个节点都能被解析编译,通过使用{{}}表达式声明文本节点。

  • 相关指令通过特定的前缀进行标记如v-test@clickv-model

  • 对于普通属性,如placeholder等普通属性,不进行相关处理。

  • 真正用于解析指令的方法通过compileUtil.bind(node, vm, exp, dir)来实现,在节点更新时,自动调用updater[xxx + 'Updater']( node, value, oldValue );更新相应的界面。

上面代码有点多,直接online-copy的。有空的时候再缩一下。

目录

【参考文章】