# 数据劫持
# 原理流程图
# 初始化 init
将创建Vue对象时传入的 options
对象和options中的 data
挂载到vm示例上
function Vue(options) {
this._init(options)
}
Vue.prototype._init = function (options) {
const vm = this
// 将options对象挂载到vm示例上
vm.$options = options
initState(vm)
}
function initState(vm) {
var options = vm.$options
if (options.data) {
// 初始化data
initData(vm)
}
}
function initData(vm) {
var data = vm.$options.data
// 将data()函数返回的数据绑定到实例的_data上
vm._data = data = typeof data == 'function' ? data.call(vm) : data || {}
for (var key in data) {
// 代理数据
proxyData(vm, '_data', key)
}
// 使用数据的观察者观察数据
observe(vm._data)
}
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
将 options
以 $options
形式, data
以 _data_
形式,挂载在实例上
# 代理 proxy
代理就是使用 vm.xxx
去获取或更改 vm._data.xxx
,同时也将 data
上的所有属性挂载在实例上
function proxyData(vm, target, key) {
// 此处的前两个参数可以理解为:vm.xxx(key为.后xxx)
Object.defineProperty(vm, key, {
// 使用代理后可以直接使用vm.xxx来获取或修改vm._data.xxx
get() {
return vm[target][key]
},
set(newValue) {
vm[target][key] = newValue
}
})
}
// 官方源码
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 观察者 observer
对data及其内部的属性进行观察,由于对对象和数组的处理方法不一样,需要分两部分处理:
- 处理对象使用
Object.defineProperty
方法
function observe(data) {
// 只观察引用类型,即对象或数组
if (typeof data !== 'object' || data === null) return
var ob
// 已被观察则返回data.__ob__,省去多余操作
if (data.__ob__ && data.__ob__ instanceof Observer) {
ob = data.__ob__;
}
else {
ob = new Observer(data)
}
return ob
}
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
function Observer(data) {
// 将Observer对象挂载到被观察的对象上,标识是否已被观察
def(data, '__ob__', this);
if (Array.isArray(data)) {
data.__proto__ = arrMethods
observeArr(data)
}
else {
this.walk(data)
}
}
Observer.prototype.walk = function (data) {
var keys = Object.keys(data)
// 遍历data的所有key和value
for (var i = 0; i < keys.length; i++) {
var key = keys[i],
value = data[key]
defineReactiveData(data, key, value)
}
}
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
- 添加响应式数据处理
function defineReactiveData(data, key, value) {
// 递归观察
observe(value)
Object.defineProperty(data, key, {
get() {
// 响应式数据获取
console.log('响应式数据获取: ', value);
return value
},
set(newValue) {
// 响应式数据设置
console.log('响应式数据设置');
if (newValue === value) return
// 观察新值
observe(newValue)
value = newValue
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 若是数组,则需要对数组方法进行拦截,重写所有会改变原数组的方法
// 存放所有会改变原数组的方法
var ARR_METHODS = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 继承Array类
var originArrMethods = Array.prototype,
arrMethods = Object.create(originArrMethods)
// 遍历ARR_METHODS,一个一个重写里面的方法
ARR_METHODS.forEach(m => {
arrMethods[m] = function () {
console.log('数组新方法', arguments);
var args = Array.prototype.slice.call(arguments),
rt = originArrMethods[m].apply(this, args)
var newArr
switch (m) {
case 'push':
case 'unshift':
newArr = args
break
case 'splice':
newArr = args.slice(2)
break
default:
break
}
newArr && observeArr(newArr)
return rt
}
})
function observeArr(arr) {
for (var i = 0; i < arr.length; i++) {
observe(arr[i])
}
}
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
重写所有会改变原数组的方法后:
# 模板编译
# 原理流程图
# 编译入口
通过el或template或render来获取HTML模板(template)
let inBrowser = typeof window !== 'undefined';
Vue.prototype.$mount = function (el) {
const vm = this,
options = vm.$options
el = el && inBrowser ? document.querySelector(el) : undefined
vm.$el = el
if (!options.render) {
let template = options.template
if (!template && el) {
template = el.outerHTML
}
const render = compileToRenderFunction(template)
options.render = render
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 渲染函数编译入口
生成渲染函数,需要先将HTML模板解析为AST树,再由AST树通过拼接字符串的方式去生成渲染函数的函数体字符串,最后通过 new Function()
传入函数题字符串来生成渲染函数
function compileToRenderFunction(html) {
const ast = parseHtmlToAST(html),
code = ast ? generate(ast) : '_c("div")',
render = new Function(`
with(this){ return ${code} }
`)
return render
}
2
3
4
5
6
7
8
9
生成渲染函数的过程(第一个为HTML模板字符串):
# 构建AST树
通过获取的template来生成AST树(Abstract Syntax Tree 抽象语法树)
- 使用正则去匹配标签名、属性等:
// 匹配属性:id="app" | id='app | id=app
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配标签名
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配开始标签:<div
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 匹配结束标签:> | />
const startTagClose = /^\s*(\/?)>/
// 匹配整个结束标签:</div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
2
3
4
5
6
7
8
9
10
11
使用解析函数将HTML模板转换为AST树,工作原理:
从头开始匹配第一个标签或文本,每匹配到一次就将匹配到的标签或文本解析为一个AST树节点,并将其从HTML模板中删去;
若匹配到的是一个开始标签,则将标签名压入栈中;
若为结束标签,则对应的解析出的AST树节点将会以相应的父子关系添加进AST树中,然后继续从头匹配下一个标签或文本,直到HTML模板长度为0结束
function parseHtmlToAST(html) {
let text,
root,
currentParent,
stack = []
while (html) {
let textEnd = html.indexOf('<')
// 以 < 开头时才去匹配标签
if (textEnd === 0) {
const startTagMatch = parseStartTag()
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
const endTagMatch = html.match(endTag)
if (endTagMatch) {
advance(endTagMatch[0].length)
end(endTagMatch[1])
continue
}
}
if (textEnd > 0) {
text = html.substring(0, textEnd)
}
if (text) {
advance(text.length)
chars(text)
}
}
// 通过开始标签(例如:<div)来匹配到每一个标签
function parseStartTag() {
let end,
attr
const start = html.match(startTagOpen)
// match()匹配失败时返回一个null
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
// 获取标签的所有属性
// 赋值语句会返回所赋给变量的值,以此判断是否匹配到相应内容
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
})
advance(attr[0].length)
}
if (end) {
advance(end[0].length)
return match
}
}
}
function advance(n) {
html = html.substring(n)
}
// 匹配到开始标签,则将该标签压入栈中
function start(tagName, attrs) {
const element = createASTElement(tagName, attrs)
if (!root) {
root = element
}
// 将当前节点存为currentParent,以便进入子节点后使用
currentParent = element
stack.push(element)
}
// 匹配到结束标签时,将该标签弹出栈,设置其父节点为当前父节点,并存入当前父节点的子节点数组中
function end(tagName) {
const element = stack.pop()
currentParent = stack[stack.length - 1]
if (currentParent) {
element.parent = currentParent
currentParent.children.push(element)
}
}
// 处理纯文本,直接存入当前父节点的子节点数组中
function chars(text) {
text = text.trim()
if (text.length > 0) {
currentParent.children.push({
type: 3,
text
})
}
}
// 生成AST树节点
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: 1,
children: [],
attrs,
parent
}
}
return root
}
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
解析过程中,HTML模板字符串的变化(部分):
栈内元素的变化情况:
最终解析出的AST树:
# 生成渲染函数
function generate(el) {
let children = getChildren(el)
let code = `_c('${el.tag}', ${el.attrs.length > 0
?
`${formatProps(el.attrs)}`
:
'undefined'
}${children ? `,${children}` : ''
})`
return code
}
2
3
4
5
6
7
8
9
10
11
12
# 虚拟DOM
通过AST树解析出虚拟DOM,再用 patch()
函数以打补丁的形式将虚拟DOM转换为真实的DOM
function renderMixin(Vue) {
// 创建节点函数
Vue.prototype._c = function () {
return createElement(...arguments)
}
// 处理{{ }}中的值,若为对象则转为字符串
Vue.prototype._s = function (value) {
if (value === null) return
return typeof value === 'object' ? JSON.stringify(value) : value
}
// 处理纯文本内容
Vue.prototype._v = function (text) {
return createTextVnode(text)
}
Vue.prototype._render = function () {
const vm = this,
render = vm.$options.render,
vnode = render.call(vm)
return vnode
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25