1.0 如何监听一个对象的变化

1.Object.defineProperty方法//ES5中新增->可以自定义getter和setter函数,从而在
获取对象属性和设置对象属性的时候能够执行自定义的回调函数。
2.对象是个层次的结构,对象的某个属性可能仍是一个对象。
1
2
3
4
5
6
7
8
9
let data = {
user: {
name: "liangshaofeng",
age: "24"
},
address: {
city: "beijing"
}
};

解决方案:如果对象的属性仍然是一个对象,那么使用递归算法,walk函数,继续new一个Observer
直到到达最底层的属性位置。
实现代码:
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
// 观察者构造函数
function Observer(data) {
this.data = data;
this.walk(data)
}

let p = Observer.prototype;

// 此函数用于深层次遍历对象的各个属性
// 采用的是递归的思路
// 因为我们要为对象的每一个属性绑定getter和setter
p.walk = function (obj) {
let val;
for (let key in obj) {
// 这里为什么要用hasOwnProperty进行过滤呢?
// 因为for...in 循环会把对象原型链上的所有可枚举属性都循环出来
// 而我们想要的仅仅是这个对象本身拥有的属性,所以要这么做。
if (obj.hasOwnProperty(key)) {
val = obj[key];

// 这里进行判断,如果还没有遍历到最底层,继续new Observer
if (typeof val === 'object') {
new Observer(val);
}

this.convert(key, val);
}
}
};

p.convert = function (key, val) {
Object.defineProperty(this.data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('你访问了' + key);
return val
},
set: function (newVal) {
console.log('你设置了' + key);
console.log('新的' + key + ' = ' + newVal)
if (newVal === val) return;
val = newVal
}
})
};

let data = {
user: {
name: "liangshaofeng",
age: "24"
},
address: {
city: "beijing"
}
};

let app = new Observer(data);

1.上面的代码只监听了对象的变化,没有处理数组的变化。
2.当你重新set的属性是对象的话,那么新set的对象里面的属性不能调用getter和setter。
3.①实现observer②消息-订阅器③实现一个watcher④实现一个vue
4.①↑通过递归,给每个属性(包括子属性)加上get/set,有赋值触发set方法,发出通知,触发回调。
5.②↑watcher为订阅者,一旦触发notify函数,遍历watcher,调用update方法。
6.③↑实现watcher,内置构造函数,update函数(调用更新数据的函数),根据判断object.defineProperty的get是否有值来调用。
7.④↑实现一个vue,引入observer,引入watcher,observer自己的data,访问data的属性,watcher属性。
实现vue的代码如下:
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
import Watcher from '../watcher'
import {observe} from "../observer"

export default class Vue {
constructor (options={}) {
//这里简化了。。其实要merge
this.$options=options
//这里简化了。。其实要区分的
let data = this._data=this.$options.data
Object.keys(data).forEach(key=>this._proxy(key))
observe(data,this)
}


$watch(expOrFn, cb, options){
new Watcher(this, expOrFn, cb)
}

_proxy(key) {
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}

2.0 如何监听一个数组的变化

Vue.js包装了被观察数组的变异方法,故它们能触发视图更新。被包装的方法有:

1
2
3
4
5
6
7
.push()
.pop()
.shift()
.unshift()
.splice()
.sort()
.reverse()

Vue.js不能检测到下面数组变化:

1
2
.直接用索引设置元素,如vm.items[0] = {};
.修改数据的长度,如vm.items.length = 0。

修改push()函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function FakeArray() {
Array.call(this,arguments);
}

FakeArray.prototype = [];
FakeArray.prototype.constructor = FakeArray;

FakeArray.prototype.push = function () {
console.log('我被改变啦');
return Array.prototype.push.call(this,arguments);
};

let list = ['a','b','c'];

let fakeList = new FakeArray(list);

这里是类似一种继承,在继承了push函数的情况下多增一些方法。
构造函数默认返回的是this对象,而非数组,Array.call(this.arguments)这个返回的是数组。
如果直接return一个原生的Array出来,push函数就没达到重写了。


3.0如何实现一个watcher库

实现watcher库请看这

4.0如何实现动态数据绑定

1
2
3
4
5
// html
<div id="app">
<p>姓名:{{user.name}}</p>
<p>年龄:{{user.age}}</p>
</div>
1
2
3
4
5
6
7
8
9
10
// js
const app = new Bue({
el: '#app',
data: {
user: {
name: 'youngwind',
age: 24
}
}
});

直接遍历DOM模板把数据改成实际值替换,存在问题
①修改非DOM相关数据也会触发DOM的重新渲染。
②修改DOM相关属性会渲染更新整个DOM。

指令Directive
只更新数据变动相关的DOM,必须有个对象将DOM节点和对应的数据一一映射起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 指令构造函数
* @param name {string} 值为"text", 代表是文本节点
* @param el {Element} 对应的DOM元素
* @param vm {Bue} bue实例
* @param expression {String} 指令表达式,例如 "name"
* @param attr {String} 值为'nodeValue', 代表数据值对应的书节点的值
* @constructor
*/
function Directive(name, el, vm, expression) {
this.name = name; // 指令的名称, 对于普通的文本节点来说,值为"text"
this.el = el; // 指令对应的DOM元素
this.vm = vm; // 指令所属bue实例
this.expression = expression; // 指令表达式,例如 "name"
this.attr = 'nodeValue';
this.update();
}

// 这是指令的更新方法。当对应的数据发生改变了,就会执行这个方法
// 可以看出来,这个方法就是用来更新nodeValue的
Directive.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.expression];
console.log(`更新了DOM-${this.expression}`);
};

实现思路:遍历DOM节点时,会匹配出表达式,新建空的textNode,插入到这个节点前面,
然后remove掉这个文本节点。

存在问题:每次数据发生改变的时候,都需要循环directive,匹配表达式值才能找到指令,
效率很低。且根据$watch对应的回调函数与DOM无关,不会有el和attr。

解决思路:引入binding和watcher这两个类。
每个属性上都有一个数组,这个数组是订阅的意思,里面存放着一系列watcher,当watcher
代表数据发生改变时,会遍历数组,执行update更新。
Watcher是一个观察容器,可以装载Directive。
根据Binding(解决键值索引)、watcher(解决$watch)、Directive(更新DOM中属性值)三者实现双向数据绑定。

5.0批处理更新DOM

频繁操作DOM是低效率的,实现多次数据更新只更新一次DOM。

官方注:每观察到数据变化,Vue会开始一个队列,将同一事件循环内所有的数据变化
缓存起来,一个watcher多次被触发,只会推入一次到队列中。下次循环,清空队列,进行
必要的DOM更新。
解决原理:Batcher,批处理类。

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
/**
* 批处理构造函数
* @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) {
if (!this.has[job.id]) {
this.queue.push(job);
this.has[job.id] = job;
if (!this.waiting) { // wating用于保护
this.waiting = true;
setTimeout(() => {
this.flush();// 更新执行的函数
});
}
}
};

/**
* 执行并清空事件队列
*/
Batcher.prototype.flush = function () {
this.queue.forEach((job) => {
job.cb.call(job.ctx);
});
this.reset();//重置队列,保证下次能更新数据
};