导航
导航
文章目录
  1. 数据响应系统的基本思路

vue技术内幕-数据响应系统

数据响应系统的基本思路

我们都知道在Vue中存在watch(观察者)。通过设置watch可以对数据进行观察,当数据发生变化时,可以执行对应的观察函数,下面为例:

1
2
3
4
5
6
7
8
9
10
var ins = new Vue({
data () {
return {
name: 'shaw'
}
}
})
ins.$watch('name', ()=>{
console.log('name数据发生修改');
});

在这个例子中,当我们使用ins.name='zhou shaw'进行修改数据时,控制台会输出name数据发生修改,现在我们将功能抽象出来:

假设我们有一个数据data

1
2
3
const data = {
name: 'shaw'
};

函数$watch,接收两个参数(要观测的key,回调函数)

1
$watch(key,()=>{...})

实现的抽象功能

1
2
3
4
5
6
7
8
9
const data = {
name: 'shaw'
};
$watch('name',()=>{
console.log('name 值发生更改');
});

data.name = 'zhou shaw';
# log: name 值发生更改

我们通过$watch函数来添加data对象数据的依赖关系,若data中的数据发生改变时出发对应watch函数。实现这样一个功能说复杂也复杂说简单也简单,说复杂是因为我们需要考虑到很多便捷情况、如重复依赖、深度观测,以及如何处理数组等多种情况。我们暂且不考虑这些边界情况,来实现一个简单的响应式系统。

首先我们需要面临的第一个问题就是,如何检测数据发生了变化,我们可以通过Object.defineProperty来对属性进行检测:

1
2
3
4
5
6
7
8
9
10
11
const data = {
name: 'shaw'
};
Object.defineProperty(data,'name', {
set (newValue) {
console.log('name 值发生更改');
},
get () {
console.log('读取了属性name');
}
});

通过defineProperty定义我们劫持了data对象的name属性操作,我们可以将劫持的方法封装至watch方法中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const data = {
name: 'shaw'
};
$watch = function (key, fn) {
Object.defineProperty(data,'name', {
set (newValue) {
fn();
},
get () {
console.log('读取了属性name');
}
});
}

上面的代码已经简单实现了对数据的观测,但是大家不难发现其中存在的问题,上面例子中set函数并未设置属性新的赋值,并且get函数并未返回获取的值会导致属性的设置和获取失效。并且上面的例子,我们我无法对一个对象的属性收集多个依赖,并且我们每次调用watch都对属性重新定义了setget,当属性还存在其他依赖时这样会覆盖原有的依赖,我们不妨展开🤔,set函数可以用于触发数据数据发生变化,我们可不可以通过get函数来进行依赖收集呢

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
var data = {
name: 'shaw',
age: 23
};

let Target;

let dep = [];

let val = data['name'];
Object.defineProperty(data, 'name', {
set(newValue) {
// 设置新值与旧值相等,不进行依赖
if (val === newValue) return;
val = newValue
// 执行依赖
dep.forEach(fn => fn());
},
get() {
// 有依赖进行收集
if (Target) dep.push(Target);
return val;
}
});

$watch = function (key,fn) {
Target = fn;
data[key];
}

$watch('name', () => {
console.log('设置了name');
})

$watch('name', () => {
console.log('多重依赖,啦啦啦');
})

data.name = 'zhou shaw';

上述代码并未对其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
26
27
var data = {
name: 'shaw',
age: 23
};

let Target; // 用于缓存依赖函数

for (let key in data) {
let dep = [];
let val = data[key];
Object.defineProperty(data, key, {
set(newValue) {
if (val === newValue) return; // 设置新值与旧值相等,不进行依赖
val = newValue
dep.forEach(fn => fn()); // 执行依赖
},
get() {
if (Target) dep.push(Target); // 有依赖进行收集
return val;
}
});
}

let $watch = function (key,fn) {
Target = fn;
data[key];
}

但如果数据结构是这样呢:

1
2
3
4
5
6
7
var data = {
name: 'shaw',
infos: {
phone: '17xxx',
wechat: 'xxx'
}
};

我们会发现我们并没有对深层次对象监听,如果数据结构更为复杂呢,我们可以将数据拦截方法封装成一个函数,对数据对象进行递归遍历,将所有属性都添加依赖,

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

let Target; // 用于缓存依赖函数

function walk(data) {
for (let key in data) {
let dep = [];
let val = data[key];
// 当数据为对象类型时递归遍历
if (Object.prototype.toString.call(val) === '[object Object]') {
walk(val);
}

Object.defineProperty(data, key, {
set(newValue) {
if (val === newValue) return; // 设置新值与旧值相等,不进行依赖
val = newValue
dep.forEach(fn => fn()); // 执行依赖
},
get() {
if (Target) dep.push(Target); // 有依赖进行收集
return val;
}
});
}
}

walk(data);

let $watch = function (key,fn) {
Target = fn;
data[key];
}

尽管对数据进行深度观察了,但我们会发现,我们的watch函数并不会对infos.wechat进行观察,所以我们需要对$watch函数进行改造,让其支持infos.wechat依赖收集,所以我们想实现的效果是:

1
2
3
$watch('infos.wechat', () => {
console.log('修改了wechat');
})

由于我们已经实现了数据进行访问即可收集依赖,但我们无法直接通过infos.wechat收集,我们需要将其转换成data['infos']['wechat']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var data = {
name: 'shaw',
infos: {
wechat: '466'
}
};

let $watch = function (key,fn) {

Target = fn;

if (/\./.test(key)) {
let paths = key.split('.');
let obj = data;
paths.forEach((path) => {
obj = obj[path];
});
return;
}

data[key];
}

到这里为止我们已经实现了一个简单的数据依赖收集系统,我们如何实现dom节点和数据绑定式渲染呢,在vue中模板最终都会生成一个render函数,通过这个函数来实现最终的渲染。若render函数中访问了data数据,我们可以观察render函数中的data数据,若render函数中访问的数据发生变化,则执行render函数进行重新渲染,那么如何收集render函数中访问的依赖,并将render函数与数据建立联系呢,很简单我们将watch函数进行改造一下就行了

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
let $watch = function (exp,fn) {

Target = fn;
if (typeof exp==="function") {
exp();
return;
}

if (/\./.test(exp)) {
let paths = exp.split('.');
let obj = data;
paths.forEach((path) => {
obj = obj[path];
});
return;
}

data[exp];
}

function render() {
return document.write(`姓名:${data.name}; 年龄:${data.age}<br/>`)
}

$watch(render, render)

在这里我们将$watch函数进行了改造,我们将第一个参数的表达是进行了类型判断,如果是函数类型进行执行。因为我们是通过get拦截进行依赖收集的,执行函数后可以收集render函数中数据的依赖,依赖的执行函数就是render,若我们对render函数中依赖的data数据进行修改,就会触发render函数从而实现数据响应视图。当然这里的实现只是vue的基本原理,从这里我们不难看出我们实现的这个简陋的响应系统中存在的问题,当修改数据触发render函数时,又进行了重复的依赖收集,并且这里也没有针对Object.defineProperty属性无法对数组进行观察进行处理。接下来会针对这一系列问题进行处理