Vue源码分析
本文最后更新于:2021年4月8日 下午
说明
- 分析 vue 作为一个 MVVM 框架的基本实现原理
数据代理,模板解析,数据绑定 - 不直接看 vue.js 的源码
- 剖析 github 上某个仿 vue 实现的 mvvm 库
准备知识
[].slice.call(lis)
: 将伪数组转换为真数组(本质是一个对象)node.nodeType
: 得到节点类型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<body>
<div id="test">Test</div>
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
<script>
// 1.[].slice.call(lis): 将伪数组转换为真数组
const lis = document.getElementsByTagName('li') // lis是伪数组
console.log(lis instanceof Array, lis[1].innerHTML, lis.forEach);
// false "test2" undefined
const lis2 = Array.prototype.slice.call(lis)
console.log(lis2 instanceof Array, lis2[1].innerHTML, lis2.forEach);
// true "test2" ƒ forEach() { [native code] }
// 2. node.nodeType: 得到节点类型
const elementNode = document.getElementById('test')
const attrNode = elementNode.getAttributeNode('id')
const textNode = elementNode.firstChild
console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType)
// 1 2 3
</script>
</body>Object.defineProperty(obj, propertyName, {})
: 给对象添加属性(指定描述符)
可参考这里Object1
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
50const obj = {
firstName: 'A',
lastName: 'B'
}
// 给obj添加fullName属性
// obj.fullName = 'A-B',赋值变化后不能改变
/*
属性描述符:
1. 数据描述符
configurable: 是否可以重新定义
enumerable: 是否可以枚举
value: 初始值
writable: 是否可以修改属性值
2. 访问描述符
get: 回调函数,根据其它相关的属性动态计算得到当前属性值
set: 回调函数,监视当前属性值的变化,更新其它相关的属性值
*/
Object.defineProperty(obj, 'fullName', {
get() {
return this.firstName + '-' + this.lastName
},
set(value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
})
console.log(obj.fullName); // A-B
obj.firstName = 'C'
obj.lastName = 'D'
console.log(obj.fullName); // C-D
obj.fullName = 'E-F'
console.log(obj.firstName, obj.lastName); // E F
Object.defineProperty(obj, 'fullName2', {
configurable: false,
enumerable: true,
value: 'G-H',
writable: false
})
console.log(obj.fullName2); // G-H
obj.fullName2 = 'J-k'
console.log(obj.fullName2); // G-H
/* Object.defineProperty(obj, 'fullName2', { // 不能重新定义
configurable: false,
enumerable: false,
value: 'G-H',
writable: true
}) */Object.keys(obj)
: 得到对象自身可枚举属性组成的数组1
2
3
4const names = Object.keys(obj)
console.log(names); // ["firstName", "lastName", "fullName2"]
// 结果里没有fullName
// enumerable默认为false,不能枚举obj.hasOwnProperty(prop)
: 判断prop是否是obj自身的属性1
2console.log(obj.hasOwnProperty('fullName'), obj.hasOwnProperty('.toString'));
// true falseDocumentFragment
: 文档碎片(高效批量更新多个节点)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// document: 对应显示的页面,包含n个element。一旦更新document内部的某个元素,界面更新。
// documentFragment: 内存中保存n个element的容器对象(不与界面关联),如果更新fragment中的某个element,界面不变
/* <ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul> */
const ul = document.getElementById('fragment_test')
// 1. 创建fragment
const fragment = document.createDocumentFragment()
// 2. 取出ul中所有子节点保存到fragment
let child
while (child = ul.firstChild) { // 一个节点只能有一个父亲
fragment.appendChild(child) // 先将child从ul中移除,添加为fragment子节点
}
// 3. 更新fragment中所有li的文本
Array.prototype.slice.call(fragment.childNodes).forEach(node => {
if (node.nodeType == 1) { // 元素节点<li>
node.textContent = 'modify'
}
})
// 4. 将fragment插入ul
ul.appendChild(fragment)
1. 数据代理
- vue数据代理: data对象的所有属性的操作(读/写)由vm对象来代理操作
- 好处: 通过vm对象就可以方便的操作data中的数据
- 实现:
- 通过Object.defineProperty(vm, key, {})给vm添加与data对象的属性对应的属性
- 所有添加的属性都包含get/set方法
- 在get/set方法中去操作data中对应的属性
1 |
|
1 |
|
2. 模板解析
1 |
|
- 将 el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中
- 对 fragment 中的所有层次子节点递归进行编译解析处理
- 对大括号表达式文本节点进行解析
- 对元素节点的指令属性进行解析
- 事件指令解析
- 一般指令解析
- 将解析后的 fragment 添加到 el 中显示
- 大括号表达式解析
- 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1(name)
- 从 data 中取出表达式对应的属性值
- 将属性值设置为文本节点的 textContent
- 事件指令解析
- 从指令名中取出事件名
- 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
- 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
- 指令解析完后, 移除此指令属性
- 一般指令解析
- 得到指令名和指令值(表达式)
- text/html/class
- msg/myClass
- 从 data 中根据表达式得到对应的值
- 根据指令名确定需要操作元素节点的什么属性
- v-text—textContent 属性
- v-html—innerHTML 属性
- v-class–className 属性
- 将得到的表达式的值设置到对应的属性上
- 移除元素的指令属性
- 得到指令名和指令值(表达式)
1 |
|
1 |
|
1 |
|
3. 数据绑定
- 数据绑定
一旦更新了data中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新。 - 数据劫持
- 数据劫持是 vue 中用来实现数据绑定的一种技术
- 基本思想: 通过 defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
- 四个重要对象
- Observer
- 用来对 data 所有属性数据进行劫持的构造函数
- 给 data 中所有属性重新定义属性描述(get/set)
- 为 data 中的每个属性创建对应的 dep 对象
- Dep(Depend)
- data 中的每个属性(所有层次)都对应一个 dep 对象
- 创建的时机:
- 在初始化 define data 中各个属性时创建对应的 dep 对象
- 在 data 中的某个属性值被设置为新的对象时
- 对象的结构
1
2
3
4{
id, // 每个 dep 都有一个唯一的 id
subs // 包含 n 个对应 watcher 的数组(subscribes 的简写)
} - subs 属性说明
- 当 watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs 中
- 当此 data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知, 从而最终更新对应的界面
- Compiler
- 用来解析模板页面的对象的构造函数(一个实例)
- 利用 compile 对象解析模板页面
- 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
- complie 与 watcher 关系: 一对多的关系
- Watcher
- 模板中每个非事件指令或表达式都对应一个 watcher 对象
- 监视当前表达式数据的变化
- 创建的时机: 在初始化编译模板时
- 对象的组成
1
2
3
4
5
6
7
8{
vm, // vm 对象
exp, // 对应指令的表达式
cb, // 当表达式所对应的数据发生改变的回调函数
value, // 表达式当前的值
depIds // 表达式中各级属性所对应的 dep 对象的集合对象
// 属性名为 dep 的 id, 属性值为 dep
}
- Observer
- 总结: dep 与 watcher 的关系: 多对多
- data 中的一个属性对应一个 dep, 一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)
- 模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep(表达式是多层: a.b)
- 数据绑定使用到 2 个核心技术
- defineProperty()
- 消息订阅与发布
1 |
|
1 |
|
1 |
|
1 |
|
MVVM原理
双向数据绑定
- 双向数据绑定是建立在单向数据绑定(model => View)的基础之上的
- 双向数据绑定的实现流程:
- 在解析 v-model 指令时, 给当前元素添加 input 监听
- 当 input 的 value 发生改变时, 将最新的值赋值给当前表达式所对应的 data 属性
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// compile.js
var compileUtil = {
// 解析v-model
model: function (node, vm, exp) {
// 实现数据的初始化显示和创建对应watcher
this.bind(node, vm, exp, 'model');
var me = this,
// 得到表达式的值
val = this._getVMVal(vm, exp);
// 给节点绑定input事件监听(输入改变时)
node.addEventListener('input', function (e) {
// 得到输入的最新值
var newValue = e.target.value;
// 如果没有变化,直接结束
if (val === newValue) {
return;
}
// 将最新value保存给表达式所对应的属性
me._setVMVal(vm, exp, newValue);
// 保存最新的值
val = newValue;
});
}
}
函数实现
observe
1 |
|
1 |
|
mapState
1 |
|
web前端学习笔记15——Vue源码分析
http://example.com/posts/c12f9186.html