Vue的响应式数据是如何做到的?

变化侦测

  • 变化侦测 = 数据观测+依赖收集+依赖更新

    1. 使用Object.defineProperty来使得数据变得可“观测”
    2. 依赖收集(Observer):是指收集视图里的部分与数据绑定的关系
    3. 在getter中收集依赖,在setter中通知更新依赖
    4. 典型的发布-订阅模式,为了解耦,新增了一个管理对象
    5. dep(收集某个数据相关的所有依赖),watcher(被dep通知,更新依赖)
    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
    // observer.js
    // 收集依赖
    const Dep = require('./dep');

    export class Observer {
    constructor(value) {
    this.value = value;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
    console.log("array");
    } else {
    this.walk(value);
    }
    }

    walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
    defineReactive(obj,keys[i]);
    }
    }
    }


    function defineReactive(obj, key, val) {

    if (arguments.length === 2) {
    val = obj[key];
    }

    if (typeof val === 'object') {
    new Observer(val);
    }

    const dep = new Dep();

    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
    dep.depend();
    return val;
    },
    set(newval) {
    if (val === newval) return;
    val = newval;
    dep.notify();
    }
    })
    }
    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
    // dep.js

    // 依赖管理器: 1数据 :n依赖 的一对多关系进行依赖管理,收集某个数据相关的所有依赖

    export default class Dep {

    constructor() {
    this.subs = [];
    }

    addSub(sub) {
    this.subs.push(sub);
    }

    removeSub(sub) {
    remove(this.subs, sub);
    }

    depend() {
    window.target && this.addSub(window.target);
    }

    notify() {
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
    subs[i].update();
    }
    }

    }


    export function remove(arr, item) {

    if (arr.length > 1) {
    const itemIndex = arr.indexOf(item);
    if (itemIndex > 1) {
    return arr.splice(itemIndex, 1);
    }
    }

    }
    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
    // watcher.js

    // watcher表示依赖关系,通知视图更新

    // window.target是为了拷贝一份 watcher,添加到Dep的依赖数组中

    export default class Watcher {
    constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn);
    this.value = this.get();
    }

    get() {
    window.target = this;
    const vm = this.vm;
    let value = this.getter.call(vm, vm);
    window.target = undefined;
    return value;
    }

    update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
    }
    }


    /**
    * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
    * 例如:
    * data = {a:{b:{c:2}}}
    * parsePath('a.b.c')(data) // 2
    */

    const bailRE = /[^\w.$]/;

    export function parsePath(path) {

    if (bailRE.test(path)) return;

    const segements = path.split('.');

    return function (obj) {
    for (let i = 0; i < segements.length; i++) {
    if (!obj) return;
    obj = obj[segements[i]];
    }
    return obj;
    }

    }

    侦测流程

vue这套变化侦测的缺点很明显,因为利用defineProperty来进行收集,只限于读和写已有值,当我们对obj进行新增或者删除属性值时,它是监听不到的。所以在官网文档上的叙述上说明过,对数组或对象的直接增加或者删除会产生不期望的结果, 为了解决这一问题,特地增加了Vue.setVue.delete两个全局API 。

数组怎么办?

看到这里,对原型熟悉的人可能会问了,这种方法只针对于Obj类型,那剩下的常用的Arr类型或者其他类型呢?defineProperty数组是不可能使用的,那么我们应该怎么对数组进行依赖收集和通知更新?

还是延续上面的思想:拦截,vue将所有数组的异变方法(能改变原有数组)拦截一波,就能知道arr啥时候被setter了。

经常面试被问到原型,原型链的what,why,那么how???? 我觉得这就是个很巧妙的实践~

拦截数组原型上的异变方法(会改变原有宿主的方法)的代码:

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
//代码位置 vue/src/core/observer/array.js

/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)

// notify change
ob.dep.notify()
return result
})
})

数组依赖收集

无论怎样,先得用walk让元素注入observer依赖,使得在getter中实例化Dep收集依赖并将数组方法拦截掉

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
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
// 源码位置:/src/core/observer/index.js
const Dep = require("./dep");

const { arrayKeys, arrayMethods } = require("./array");

// 源码位置:src/core/observer/index.js

// 使用 defineProperty 让数据可观测

export class Observer {

constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
if (Array.isArray(value)) {
const agument = hasProto ? protoAugment : copyAugument;
[agument](value, arrayMethods, arrayKeys);
this.observerArray(value);
} else {
this.walk(value);
}
}

walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}

observerArray(ietms) {
for (let i = 0; i < ietms.length; i++) {
observe(ietms[i]);
}
}
}

export const hasProto = "__proto__" in {};

/*
复制原型属性,添加拦截
*/

function protoAugment(target, src, keys) {
target.__proto__ = src;
}

function copyAugument(target, src, keys) {
for (let i = 0; i < keys.length; i++) {
const key = key[i];
def(target, key, src[key]);
}
}

/*
* 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
* 如果 Value 已经存在一个Observer实例,则直接返回它
*/

function observe(value) {
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}

function defineReactive(obj, key, val) {
let childOb = observe(val);

if (arguments.length === 2) {
val = obj[key];
}

if (typeof val === "object") {
new Observer(val);
}

const dep = new Dep();

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (childOb) {
childOb.dep.depend();
}
return val;
},
set(newval) {
if (val === newval) return;
val = newval;
dep.notify();
}
});
}

通知更新

主要是还要对数组进行深度监测和新增元素侦测,在拦截的原型上进行依赖更新。

__ob__是在进行初始化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
// 源码位置:vue/src/core/observer/array.js


/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
  • 总结: vue的变化侦测与React对比Vdom和Angular的脏值检测都不一样。核心是利用defineProperty的能力,拦截所有绑定的响应式数据(data中),在拦截中添加依赖管理器Dep来收集管理依赖,用Watcher表示依赖关系本身,进行通知依赖更新。

    其中,对于数组的侦测的思路是,覆盖所有数组原型的的异变方法,在覆盖后植入依赖逻辑。这套缺点就是对数组进行下标赋值操作时,vue是侦测不到的,官网文档上多处对此有说明。

    相信下次,面试官问:为什么在vue中对数组下标进行赋值操作会导致不正确的响应式数据结果。这种类似的问题,你一定胸有成竹。

评论