# 全家桶
vue-router、vuex、vue-cli、vue-loader、vue-server-render、vue-rx、vue-devtools
# 构成
https://github.com/vuejs/vue
主要核心代码 Vue(src/core+platforms)
主要分析core下面的代码 components模版编译代码、 global-api最上层的文件接口、 instance生命周期->init.js、 observer数据收集与订阅、 util常用工具方法类、 vdom虚拟DOM
# 双向数据绑定
1、Object.defineProperty 2、Observer 3、Watcher 4、Dep //Dep是订阅者Watcher对应的数据依赖 5、Directive
每一个directive创建一个watcher,当times改变时,observer 结合大爷卖报纸
# Object.defineProperty
var obj = {};
var a='12345';
Object.defineProperty(obj, 'a', {
get: function () {
console.log('get val:'+a);
return a;
},
set: function (newVal) {
console.log('set newVal:' + newVal);
return a = newVal;
}
})
obj.a;//get val:12345 <span>{{a}}</span>
obj.a = '54321';//set newVal:54321 <input v-model="a">
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这就是主要原理,也是vue不兼容低版本ie的原因 解决方案: 最早的百度框架 san.js
__defineGetter__
ƒ __defineGetter__() { [native code] }
__defineSetter__
ƒ __defineSetter__() { [native code] }
2
3
4
微软VBScript,拥有类,类可以get、set,可以操作打印机office等
# Observer
观察者模式是软件设计中的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
订阅者模式涉及三个对象:发布者、主题对象、订阅者,三个对象间是一对多的关系,每当主题对象发生改变时,其相关依赖对象都会得到通知,并自动更新。
//主题对象
function Dep() {
//订阅者列表
this.subs = [];
}
//主题对象通知订阅者
Dep.prototype.notify = function () {
//遍历所有的订阅者,执行订阅者提供的更新方法
this.subs.forEach(function (sub) {
sub.update();
})
}
//订阅者
function Sub(x) {
this.x = x;
}
//订阅者更新
Sub.prototype.update = function () {
this.x = this.x + 1;
console.log(this.x)
}
//发布者
var pub = {
publish: function () {
dep.notify();
}
}
//主题对象实例
var dep = new Dep();
//新增2个订阅者
dep.subs.push(new Sub(1), new Sub(2));
//发布者发布更新
pub.publish();
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
# 源码阅读
# 阅读思路
1、index.html
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
2
3
4
5
6
2.MVVM.js
observe(data, this);
var dom = new Compile(document.getElementById(id), this);
2
3、Observe.js
var dep = new Dep();
defineReactive(vm, key, obj[key]);//Vue text 'hello world'
Dep? -> Dep.target
2
3
4、Compile.js
while (child = node.firstChild) {
self.compileElement(child, vm);
// 将所有子节点添加到fragment中 拿一层削一层 避免了使用递归
frag.append(child);
console.log(frag);
}
return frag;
var reg = /\{\{(.*)\}\}/; //{{text}}
new Watcher(vm, node, name, 'nodeValue');
2
3
4
5
6
7
8
9
10
11
5、Watcher.js
//如何让watcher和Dep建立联系才是关键
get: function () {
this.value = this.vm[this.name]; //触发相应属性的get
}
Object.defineProperty(vm, key, {
get: function () {
if (Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val; //就是上面的this.value 'hello world'
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
vim vue实例一直被各个文件传来传去 同时也被监听Object.defineProperty(vm, key)
# 简单实现
按照阅读思路步骤进行
# index.html
<!DOCTYPE html>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
<script>
window.batcher = "";
</script>
<script src="src/Batcher.js"></script>
<script src="src/Dep.js"></script>
<script src="src/Observe.js"></script>
<script src="src/Watcher.js"></script>
<script src="src/Compile.js"></script>
<script src="src/MVVM.js"></script>
<script>
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
// for (var i = 0; i < 100; i++) {
// vm["text"] = i;
// }
</script>
</body>
</html>
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
# Dep.js
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# MVVM.js
/**
{
el: 'app',
data: {
text: 'hello world'
}
}
*/
function Vue(options) {
//data转移减少作用链的查找 text: 'hello world'
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = new Compile(document.getElementById(id), this);
// 编译完成后,将dom返回到app中 nodeToFragment方法写的虚拟DOM
document.getElementById(id).appendChild(dom);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Observe.js
function defineReactive(vm, key, val) {
var dep = new Dep();
Object.defineProperty(vm, key, {
get: function () {
if (Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if (newVal === val) return;
val = newVal;
// 作为发布者发出通知
dep.notify();
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
//Vue{} text 'hello world'
defineReactive(vm, key, obj[key]);
})
}
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
function Compile(node, vm) {
//<div id='app'></div> Vue{}
if (node) {
//代码片段在内存中存在一个虚拟DOM 但是现在的虚拟DOM是一个对象
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function (node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
self.compileElement(child, vm);
// 将所有子节点添加到fragment中 拿一层削一层 避免了使用递归
frag.append(child);
}
return frag;
},
compileElement: function (node, vm) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
// 获取v-model绑定的属性名
var name = attr[i].nodeValue;
node.addEventListener('input', function (e) {
// 给相应的data属性赋值,进而触发该属性的set方法
//再批处理 渲染元素
vm[name] = e.target.value;
});
// node.value = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name, 'value');
}
}
}
//节点类型为text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
// console.log(node);//'hello world'
// 获取匹配到的字符串
var name = RegExp.$1;
name = name.trim();//{{}}里的text
// node.nodeValue = vm[name]; 将data的值赋给该node
new Watcher(vm, node, name, 'nodeValue');
}
}
}
}
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
# Watcher.js
let uid = 0;
function Watcher(vm, node, name, type) {
//new Watcher(vm, node, name, 'nodeValue');
Dep.target = this;
this.name = name; //text
this.id = ++uid;
this.node = node; //当前的节点
this.vm = vm; //vm
this.type = type; //nodeValue
this.update();
Dep.target = null;
}
// function queueWatcher(watcher){
// var id = watcher.id;
// if(has[id]==null){
// }
// }
Watcher.prototype = {
update: function () {
this.get();
if (!batcher) {
batcher = new Batcher();
// console.log(this.node);
// this.node[this.type] = this.value;
}
batcher.push(this);
//span.nodeValue = this.vm.text
// this.node[this.type] = this.value; // 订阅者执行相应操作
},
cb: function () {
//最终实际虚拟dom处理的结果 只处理一次
console.log("dom update");
this.node[this.type] = this.value; // 订阅者执行相应操作
},
// 获取data的属性值
get: function () {
this.value = this.vm[this.name]; //触发相应属性的get
}
}
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
# 存在问题
1、输入框疯狂输入,一变就产生一个watcher 2、一变化就会导致页面变化把ui堵死
for (var i = 0; i < 100; i++) {
vm["text"] = i;
}
2
3
# Batcher
批处理是如何进行的
index.html
window.batcher = "";//全局定义一个batcher
watcher.js
let uid = 0;
function Watcher(vm, node, name, type) {
//new Watcher(vm, node, name, 'nodeValue');
Dep.target = this;
this.name = name; //text
this.id = ++uid; //重点重点重点重点重点重点重点重点重点重点重点重点重点重点重点重点
this.node = node; //当前的节点
this.vm = vm; //vm
this.type = type; //nodeValue
this.update();
Dep.target = null;
}
if (!batcher) {
batcher = new Batcher();//判断有没有 没有就创建一个
// console.log(this.node);
// this.node[this.type] = this.value;
}
batcher.push(this);//把watcher放到批处理去
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Batcher.js
对应源码里的observer/scheduler.js. 死盖出了
最后一行nextTick(flushSchedulerQueue) ———>until/next-tick.js
nextTick
vue中有一个较为特殊的API,nextTick。根据官方文档的解释,它可以在DOM更新完毕之后执行一个回调,用法如下:
https://www.jianshu.com/p/a7550c0e164f
/**
* 批处理构造函数
* @constructor
*/
function Batcher() {
this.reset();
}
/**
* 批处理重置
*/
Batcher.prototype.reset = function () {
this.has = {};
this.queue = [];
this.waiting = false;
};
/**
* 将事件添加到队列中
* @param job {Watcher} watcher事件
*/
Batcher.prototype.push = function (job) {
// console.log(job);//这里不是放了99个watcher嘛 为什么id只有1、2而不是1到99
let id = job.id;//就是前面重点的uid
if (!this.has[id]) {
// console.log(batcher);
this.queue.push(job);
//设置元素的ID
this.has[id] = true;
if (!this.waiting) {
this.waiting = true;
if ("Promise" in window) {
Promise.resolve().then( ()=> {
this.flush();
})
} else {
setTimeout(() => {
this.flush();
}, 0);
}
}
}
};
/**
* 执行并清空事件队列
*/
Batcher.prototype.flush = function () {
this.queue.forEach((job) => {
job.cb();
});
this.reset();
};
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
# Vue 被玩死
<div v-if="flag">{{msg1}}</div>
<div v-else>{{msg2}}</div>
<button @click="change">变化</button>
<button @click="toggle">切换</button>
export default {
data() {
return{
msg1:'xiaoming',
msg2:'xiaohong',
flag:true
}
},
methods:{
change() {
this.msg1 = Math.random()
},
toggle() {
this.flag = !this.flag
}
},
watch:{
msg1() {
this.msg1 = Math.random();//You may have an infinite update loop in watcher with expression"msg1"
}
}
}
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
https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js 在vue的源码中会发现
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
2
3
4
5
6
7
8
9
10
11
12
13
# vue里面有时候变量的对象层级太深,修改变量会不生效
没监听到 所以vue3proxy. 回去看第一周课
Observer/arr.js index.js
先切割数组,然后遍历放到 const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 绑定
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
2
3
4
5
6
7
8
9
# 总结
← 简单实现Vue Vue2源码分析(二) →