透过迷你vue库,了解vue背后思想

the super tiny vue.js.

简介:一个迷你vue库,虽然小但功能全面,可以作为想了解vue背后思想以及想学习vue源码而又不知如何入手的入门学习资料。

特性:

  • 数据响应式更新
  • 指令模板
  • MVVM
  • 轻量级
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
/**
* the super tiny vue.js.


## 功能解读
<templete>
<div id='app'>
<div>
<input v-model='counter' />
<button v-on-click='add'>add</button>
<p v-text='counter'></p>
</div>
</div>
</templete>
<script>
var vm = new Vue({
id: 'counter',
data: {
counter: 1
},
methods: {
add: function () {
this.counter += 1;
}
}
})
</script>

如上为一段模板以及js脚本,我们所要实现的目标就是将 vm 实例与id为app的DOM节点关联起来,当更改vm data 的counter属性的时候,
input的值和p标签的文本会响应式的改变,method中的add方法则和button的click事件绑定。
简单的说就是, 当点击button按钮的时候,触发button的点击事件回调函数add,在add方法中使counter加1,counter变化后模板中的input
和p标签会自动更新。vm与模板之间是如何关联的则是通过 v-model、v-on-click、v-text这样的指令声明的。
### 实现思路详解
* 查找含指令的节点
* 对查找所得的节点进行指令解析、指令所对应的实现与节点绑定、 节点指令值所对应的data属性与前一步关联的指令实现绑定、data属性值通过setter通知关联的指令进行更新操作
* 含指令的每一个节点单独执行第二步
* 绑定操作完成后,初始化vm实例属性值
#### 指令节点查找
首先来看第一步,含指令节点的查找,因为指令声明是以属性的形式,所以可以通过属性选择器来进行查找,如下所示:
`<input v-model='counter' type='text' />`
则可通过 querySelectorAll('[v-model]') 查找即可。
root = this.$el = document.getElementById(opts.el),
els = this.$els = root.querySelectorAll(getDirSelectors(Directives))
root对于根节点,els对应于模板内含指令的节点。
#### 指令解析,绑定

* 1.指令解析
同样以`<input v-model='counter' type='text' />`为例,解析即得到
var directive = {
name: 'v-model',
value: 'counter'
}
name对应指令名,value对应指令值。
* 2.指令对应实现与当前节点的绑定(bindDirective)
指令实现可简单分为函数或是包含update函数的对象,如下便是`v-text`指令的实现代码:
text: function (el, value) {
el.textContent = value || '';
}

指令与节点的绑定即将该函数与节点绑定起来,即该函数负责该节点的更新操作,`v-text`的功能是更新文本值,所以如上所示
更改节点的textContent属性值。
* 3. 响应式数据与节点的绑定(bindAccessors)
响应式数据这里拆分为 data 和 methods 对象,分别用来存储数据值和方法。

var vm = new Vue({
id: 'counter',
data: {
counter: 1
},
methods: {
add: function () {
this.counter += 1;
}
}
})

我们上面解析得到 v-model 对于的指令值为 counter,所以这里将data中的counter与当前节点绑定。

通过2、3两步实现了类型与 textDirective->el<-data.counter 的关联,当data.counter发生set(具体查看defineProperty set 用法)操作时,
data.counter得知自己被改变了,所以通知el元素需要进行更新操作,el则使用与其关联的指令(textDirective)对自身进行更新操作,从而实现了数据的
响应式。
* textDirective
* el
* data.counter
这三个是绑定的主体,数据发生更改,通知节点需要更新,节点通过指令更新自己。
* 4.其它相关操作
*/



var prefix = 'v';
/**
* Directives
*/

var Directives = {
/**
* 对应于 v-text 指令
*/
text: function (el, value) {
el.textContent = value || '';
},
show: function (el, value) {
el.style.display = value ? '' : 'none';
},
/**
* 对应于 v-model 指令
*/
model: function (el, value, dirAgr, dir, vm, key) {
let eventName = 'keyup';
el.value = value || '';

/**
* 事件绑定控制
*/
if (el.handlers && el.handlers[eventName]) {
el.removeEventListener(eventName, el.handlers[eventName]);
} else {
el.handlers = {};
}

el.handlers[eventName] = function (e) {
vm[key] = e.target.value;
}

el.addEventListener(eventName, el.handlers[eventName]);
},
on: {
update: function (el, handler, eventName, directive) {
if (!directive.handlers) {
directive.handlers = {}
}

var handlers = directive.handlers;

if (handlers[eventName]) {
//绑定新的事件前移除原绑定的事件函数
el.removeEventListener(eventName, handlers[eventName]);
}
//绑定新的事件函数
if (handler) {
handler = handler.bind(el);
el.addEventListener(eventName, handler);
handlers[eventName] = handler;
}
}
}
}


/**
* MiniVue
*/
function TinyVue (opts) {
/**
* root/this.$el: 根节点
* els: 指令节点
* bindings: 指令与data关联的桥梁
*/
var self = this,
root = this.$el = document.getElementById(opts.el),
els = this.$els = root.querySelectorAll(getDirSelectors(Directives)),
bindings = {};
this._bindings = bindings;

/**
* 指令处理
*/
[].forEach.call(els, processNode);
processNode(root);

/**
* vm响应式数据初始化
*/

let _data = extend(opts.data, opts.methods);
for (var key in bindings) {
if (bindings.hasOwnProperty(key)) {
self[key] = _data[key];
}
}

function processNode (el) {
getAttributes(el.attributes).forEach(function (attr) {
var directive = parseDirective(attr);
if (directive) {
bindDirective(self, el, bindings, directive);
}
})
}

/**
* ready
*/
if (opts.ready && typeof opts.ready == 'function') {
this.ready = opts.ready;
this.ready();
}
}

/**************************************************************
* @privete
* helper methods
*/

/**
* 获取节点属性
* 'v-text'='counter' => {name: v-text, value: 'counter'}
*/
function getAttributes (attributes) {
return [].map.call(attributes, function (attr) {
return {
name: attr.name,
value: attr.value
}
})
}

/**
* 返回指令选择器,便于指令节点的查找
*/
function getDirSelectors (directives) {
/**
* 支持的事件指令
*/
let eventArr = ['click', 'change', 'blur'];


return Object.keys(directives).map(function (directive) {
/**
* text => 'v-text'
*/
return '[' + prefix + '-' + directive + ']';
}).join() + ',' + eventArr.map(function (eventName) {
return '[' + prefix + '-on-' + eventName + ']';
}).join();
}

/**
* 节点指令绑定
*/
function bindDirective (vm, el, bindings, directive) {
//从节点属性中移除指令声明
el.removeAttribute(directive.attr.name);

/**
* v-text='counter'
* v-model='counter'
* data = {
counter: 1
}
* 这里的 counter 即指令的 key
*/
var key = directive.key,
binding = bindings[key];

if (!binding) {
/**
* value 即 counter 对应的值
* directives 即 key 所绑定的相关指令
如:
bindings['counter'] = {
value: 1,
directives: [textDirective, modelDirective]
}
*/
bindings[key] = binding = {
value: '',
directives: []
}
}
directive.el = el;
binding.directives.push(directive);

//避免重复定义
if (!vm.hasOwnProperty(key)) {
/**
* get/set 操作绑定
*/
bindAccessors(vm, key, binding);
}
}

/**
* get/set 绑定指令更新操作
*/
function bindAccessors (vm, key, binding) {
Object.defineProperty(vm, key, {
get: function () {
return binding.value;
},
set: function (value) {
binding.value = value;
binding.directives.forEach(function (directive) {
directive.update(
directive.el,
value,
directive.argument,
directive,
vm,
key
)
})
}
})
}

function parseDirective (attr) {
if (attr.name.indexOf(prefix) === -1) return ;

/**
* 指令解析
v-on-click='onClick'
这里的指令名称为 'on', 'click'为指令的参数,onClick 为key
*/

//移除 'v-' 前缀, 提取指令名称、指令参数
var directiveStr = attr.name.slice(prefix.length + 1),
argIndex = directiveStr.indexOf('-'),
directiveName = argIndex === -1
? directiveStr
: directiveStr.slice(0, argIndex),
directiveDef = Directives[directiveName],
arg = argIndex === -1
? null
: directiveStr.slice(argIndex + 1);

/**
* 指令表达式解析,即 v-text='counter' counter的解析
* 这里暂时只考虑包含key的情况
*/
var key = attr.value;
return directiveDef
? {
attr: attr,
key: key,
dirname: directiveName,
definition: directiveDef,
argument: arg,
/**
* 指令本身是一个函数的情况下,更新函数即它本身,否则调用它的update方法
*/
update: typeof directiveDef === 'function'
? directiveDef
: directiveDef.update
}
: null;
}

/**
* 对象合并
*/
function extend (child, parent) {
parent = parent || {};
child = child || {};

for(var key in parent) {
if (parent.hasOwnProperty(key)) {
child[key] = parent[key];
}
}

return child;
}

参考来源:https://github.com/xiaofuzi/deep-in-vue/blob/master/src/the-super-tiny-vue.js