前言

本文章结合VUE核心技术-尚硅谷的49-55集做的归纳总结。

数据代理准备

1.通过一个对象代理对另一个对象中属性的操作(读/写)
  2.通过vm对象来代理data对象中所有属性的操作
  3.好处: 更方便的操作data中的数据
  4.基本实现流程
    1). 通过Object.defineProperty()给vm添加与data对象的属性对应的属性描述符
    2). 所有添加的属性都包含getter/setter
    3). 在getter/setter内部去操作data中对应的属性数据

vue运行

  vm实例中有 _data 数据,存储了Vue初始化的数据。
  name属性则是动态属性,需要在控制台点击获取

获取name

  点击其实是执行了 name 的 get 方法,这个就是代理
  set方法就是当值发生改变的时候执行

代理get

浏览器 Debug 调试

断点测试

  浏览器的控制台上 Source 标签提供了 断掉调试模式
  方便追踪代码的运行情况。

四个按钮

  浏览器上如上图的有四个按钮用来进行代码测试

  1. 第一个按钮,开启断点调试或跳到下一个断点
  2. 第二个按钮,执行下一行
  3. 第三个按钮,执行到下一个函数
  4. 第四个按钮,跳出当前函数

切换面板位置

  浏览如上图的位置可以切换控制台的摆放方式

断点

  右边可以看到当前断点运行的位置,还可以看到是什么文件以及第几行代码
  如此点击第二个按钮就可以一行一行执行代码,可以看到变量属性在执行代码之后的变化。

数据代理分析

数据代理 - 打开控制台调试

数据代理

  此处遍历data的所有数据,并执行代理函数

数据代理

  代理函数执行 defineProperty 到 vm 对象上
  这样就完成了所有数据代理,实现数据获取和更新



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
/*
相关于Vue的构造函数
*/
function MVVM(options) {
// 将选项对象保存到vm
this.$options = options;
// 将data对象保存到vm和datq变量中
var data = this._data = this.$options.data;
//将vm保存在me变量中
var me = this;
// 遍历data中所有属性
Object.keys(data).forEach(function (key) { // 属性名: name
// 对指定属性实现代理
me._proxy(key);
});

// 对data进行监视
observe(data, this);

// 创建一个用来编译模板的compile对象
this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
$watch: function (key, cb, options) {
new Watcher(this, key, cb);
},

// 对指定属性实现代理
_proxy: function (key) {
// 保存vm
var me = this;
// 给vm添加指定属性名的属性(使用属性描述)
Object.defineProperty(me, key, {
configurable: false, // 不能再重新定义
enumerable: true, // 可以枚举
// 当通过vm.name读取属性值时自动调用
get: function proxyGetter() {
// 读取data中对应属性值返回(实现代理读操作)
return me._data[key];
},
// 当通过vm.name = 'xxx'时自动调用
set: function proxySetter(newVal) {
// 将最新的值保存到data中对应的属性上(实现代理写操作)
me._data[key] = newVal;
}
});
}
};

模板解析

1.模板解析的关键对象: compile对象
  2.模板解析的基本流程:
    1). 将el的所有子节点取出, 添加到一个新建的文档fragment对象中
    2). 对fragment中的所有层次子节点递归进行编译解析处理
        * 对表达式文本节点进行解析
        * 对元素节点的指令属性进行解析
            * 事件指令解析
            * 一般指令解析
      3). 将解析后的fragment添加到el中显示
3.解析表达式文本节点: textNode.textContent = value
      1). 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1
      2). 从data中取出表达式对应的属性值
      3). 将属性值设置为文本节点的textContent
4.事件指令解析: elementNode.addEventListener(事件名, 回调函数.bind(vm))
    v-on:click="test"
      1). 从指令名中取出事件名
      2). 根据指令的值(表达式)从methods中得到对应的事件处理函数对象
      3). 给当前元素节点绑定指定事件名和回调函数的dom事件监听
      4). 指令解析完后, 移除此指令属性 
5.一般指令解析: elementNode.xxx = value
      1). 得到指令名和指令值(表达式)
      2). 从data中根据表达式得到对应的值
      3). 根据指令名确定需要操作元素节点的什么属性
        * v-text---textContent属性
        * v-html---innerHTML属性
        * v-class--className属性
      4). 将得到的表达式的值设置到对应的属性上
      5). 移除元素的指令属性

双大括号表达式

双大括号表达式 - 打开控制台调试

compile属性

  compile函数会获取绑定 el 对象,如果没有则获取 body 对象上

compile内部

  检查传入的对象是不是元素节点,如果不是元素则通过 querySelector 来获取元素,H5添加了类似JQuery选择器的功能

fragment

  将node节点转换为 fragment 容器

fragment

  正如之前 fragment 演示的一样,会将页面的内容截取掉。
  下一步就会执行 init 函数

compileElement

  init函数会执行 compileElement 函数

fragment

  compileElement 内部会获取 fragment 获取到的节点
  然后将数组转为真数组进行遍历
  通过正则表达式来匹配 双大括号

isElementNode

   isElementNode 可以过滤 div 等等 Html 节点
  然后继续进入子对象,就会获取到 div 中的内容
  如果检测是 文本信息 就通过上面的正则表达式进行匹配,由于正则表达式加入了括号,获取的内容会放到 $1 变量中
  这个正则表达式非常好用,Vscode编辑器也是一样的

正则数据

  鼠标指向可以看到 $1 存储了name数据

compileUtil

  这里会进入到 compileUtil 工具集

compileUtil

  可以看到这个工具集函数定义了很多相关的方法

bind

  执行工具集会跳转到 bind 方法,bind中使用了 updater 相关函数

updater

  updater也有很多设置好的更新方法

获取updater函数

  通过 [] 传值,动态获取updater中的函数

updaterFn

  如果 updaterFn 不存在就不执行,如果存在执行函数

_getVMVal

   _getVMVal 返回 vm._data 对象中的数据
  同时拆分了传入的变量 exp 中的内容

更新内容

  最后将返回的值赋值到 textContent 上,完成了内容的更新

事件指令

事件指令 - 打开控制台调试

HTML

  在HTML中添加按钮绑定点击事件

节点解析

  在节点解析的过程中会进入到相关属性的处理

属性

  在这里遍历标签上的属性
  判断指令是否是 directive 指令,就是判断是否含有 v-
  然后截取后两个字符的字符串

指令判断

  判断是不是事件指令 (‘on’ 开头)
  然后进入 eventHandler 函数

eventHandler

  截取 : 获取事件名
  从 methods 中获取传入的事件
  如果时间名和函数同时存在,就执行 addEventListener
  fn.bind(vm) 可以让函数的 this 指向 vm

一般指令

事件指令 - 打开控制台调试

HTML

  修改HTML标签

compileUtil

  普通指令的执行会获取到 dir 的 text 属性

compileUtil

  然后会执行到 compileUtil 的 text 函数
  再接着执行 bind 函数

updater

  updater 里面通过 textContent 更新页面内容
  通过 innerHtml 实现 HTML 标签的渲染

class

  class的更新稍微复杂一点,需要和原有的class进行合并

第五个按钮

  点击第五个按钮可以禁用 所有的断点



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
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 = {
node2Fragment: function (el) {
var fragment = document.createDocumentFragment(),
child;

// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}

return fragment;
},

init: function () {
// 编译fragment
this.compileElement(this.$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); // RegExp.$1: 表达式 name
}
// 如果子节点还有子节点
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)得到对应的更新节点函数
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) {
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;
}
};