响应式原理

提到Vue难免离不开响应式原理,在学习响应式原理之前,首先需要搞明白一个概念:何谓响应式?Vue官方文档响应式的解释是:响应性是一种允许我们以声明式的方式去适应变化的编程范例,乍一看还挺难以理解,用通俗点的大白话来说:响应式就是在数据发生变化的时候执行某个或者某些回调,使页面发生更新,也就是数据驱动页面的更新。今天我们就动手使用原生JS实现Vue的响应式原理

如何实现响应式

  1. 首先我们需要准备一个对象

    1
    2
    3
    4
    let info = {
    name:'coderwei',
    age:18
    }
  2. 首先我们可以先定义一个副作用函数,说白了就是我们需要告诉JS,当对象的属性发生变化后,你要做什么

    1
    2
    3
    4
    //副作用函数:要求传递进来一个函数,函数内部直接执行
    function watchFn(fn) {
    fn();
    }
  3. 我们需要利用Proxy拦截info对象的get和set方法想要实现响应式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    const infoProxy = new Proxy(info, {
    get(target, key, recesver) {
    console.log("获取info对象的值");
    return Reflect.get(target, key, recesver);
    },
    set(target, key, newValue, recesver) {
    console.log("设置info对象的值");
    Reflect.set(target, key, newValue, recesver);
    },
    });
  1. 然后我们在写一点测试数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // name发生变化需要执行的逻辑
    watchFn(() => {
    console.log( infoProxy.name);
    });

    // age发生变化需要执行的逻辑
    watchFn(() => {
    console.log( infoProxy.age);
    console.log("-------------");
    });
  2. 运行代码,我们可以看到当设置对象某个属性的值的时候会来到set方法,当我们获取某个属性的值的时候会来到get方法说明我们劫持成功了

  3. 于是我们就可以在这两个方法上下功夫,首先我们定义一个Depend类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Depend {
    constructor() {
    this.allFn = [];
    }
    adddepend() {
    if (activeFn) {
    this.allFn.push(activeFn);
    }
    }
    notify() {
    this.allFn.forEach((fn) => fn());
    }
    }

    //在constructor方法中定义一个函数,每次实例化这个类的时候都需要创建一个新的数组,用于放置需要执行的函数,如果我们将所有的函数都丢在一个数组内,那我们就没办法作区分了,不同的属性修改了他们是可能会有不同的回调,
    //然后在定义一个adddepend函数,用于将函数push进数组中
    //最后我们定义了一个notify的函数,用于执行allFn数组内的所有函数
  4. 我们前面说过当我们获取对象的值的时候我们会来到get方法,所以我们可以在get方法中将收集依赖,我们修改下Proxy的方法,但是这个有个问题,我们在调用set方法的adddepend时候,需要给他传递调用watchFn里面的函数,所以我们可以定义一个全局变量 ,用于保存这个函数,于是定义的watchFn的函数需要稍微修改下

    1
    2
    3
    4
    5
    6
    let activeFn = null;
    function watchFn(fn) {
    activeFn = fn;
    fn();
    activeFn = null;
    }
  5. 然后重新回到定义infoProxy的地方,再对代码进行一点修改

    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
    //首先我们先封装一个方法,用于获取每个属性对应的depend
    const weMap = new WeakMap();
    function getDepend(target, key) {
    let map = weMap.get(target);
    if (!map) {
    map = new Map();
    weMap.set(target, map);
    }

    let depend = map.get(key);
    if (!depend) {
    depend = new Depend();
    map.set(key, depend);
    }
    return depend;
    }

    //然后修改下Proxy的代码
    const infoProxy = new Proxy(info, {
    get(target, key, recesver) {
    const depend = getDepend(target, key);
    depend.adddepend(activeFn);
    return Reflect.get(target, key, recesver);
    },
    set(target, key, newValue, recesver) {
    Reflect.set(target, key, newValue, recesver);
    const depend = getDepend(target, key);
    depend.notify();
    },
    });

  6. 这里用到了WeakMap,WeakMap和Map最大的区别就是前者的键是一个对象,并且这是一个弱引用,首先我们通过get方法可以拿到这个对象的Map,判断下有没有值,第一次都是没有值的,如果没有值我们就给他初始化一个Map,然后在根据对象的key,拿到这个key所对应的依赖,当然,如果没有我们也需要初始化一个我们定义的Depend,最后将这个依赖返回出去,我们就可以在调用的时候根据对象和key拿到对应的依赖,从而执行他们对应的依赖

  7. 这里也用到了Reflect,他是ES6新增的一个内置对象,他提供了拦截JS操作的方法,为什么会出现这个方法呢?在我看来,在早期的JS中,有些东西是考虑不周到的,设计的也不合理,所以一些Objcet对象下有很多方法压根就不能属于他,放在它身上很不合适,所以导致Object这个对象特别臃肿。MDN上对Reflect的详细解释

  8. 于是我们就完成了Vue3的响应式原理,当数据发生变化的时候就会触发对应的回调

优化

  1. 紧接着我们来重构下部分代码,目前来说代码有点bug,虽然不影响功能,但是当我们watchFn中的函数多次用到了代理对象的值,那么在触发依赖的时候就会多次执行,因为每次获取一次对象的值的时候,都会来到get方法,然后将这个函数存起来,明明都是同一个函数并且还是同一个key的依赖,没必要多次执行浪费性能。所以我们可以将数组更换为set
  2. 其次就是当我们有多个对象需要拦截的时候,难道我们都写一次Proxy方法吗?明显不现实,所以我们可以将拦截对象对的get和set方法的操作封装成一个函数

最终代码

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
let activeFn = null;
class Depend {
constructor() {
this.allFn = new Set();
}
adddepend() {
if (activeFn) {
this.allFn.add(activeFn);
}
}
notify() {
this.allFn.forEach((fn) => fn());
}
}

function watchFn(fn) {
activeFn = fn;
fn();
activeFn = null;
}
let info = {
name: "coderwei",
age: 18,
};

const weMap = new WeakMap();
function getDepend(target, key) {
let map = weMap.get(target);
if (!map) {
map = new Map();
weMap.set(target, map);
}

let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}

//然后修改下Proxy的代码
function reactive(obj) {
return new Proxy(info, {
get(target, key, recesver) {
const depend = getDepend(target, key);
depend.adddepend(activeFn);
return Reflect.get(target, key, recesver);
},
set(target, key, newValue, recesver) {
Reflect.set(target, key, newValue, recesver);
const depend = getDepend(target, key);
depend.notify();
},
});
}

const infoProxy = reactive(info);

// name发生变化需要执行的逻辑
watchFn(() => {
console.log("name:", infoProxy.name);
});

// age发生变化需要执行的逻辑
watchFn(() => {
console.log("age:", infoProxy.age);
console.log("-------------");
});

infoProxy.age = 99988;

Vue2的响应式

  1. 基本上大同小异,只是劫持对象用的方法不一样,Vue3用的Proxy,Vue2用的Object.defineProperty,唯一不同的地方也就在这里,我就直接贴上我的代码了,如果有疑问可以联系我

    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
    // 勿在浮沙筑高
    let activeFn = null;

    // 封装一个Depend类 用于收集依赖
    class Depend {
    constructor() {
    this.allFn = new Set();
    }
    addDepend() {
    if (activeFn) {
    this.allFn.add(activeFn);
    }
    }
    notify() {
    this.allFn.forEach((fn) => fn());
    }
    }

    let info = {
    name: "coderwhy",
    age: 18,
    };

    // vue2 ====> Object.defineProperty
    function reactive(obj) {
    Object.keys(obj).forEach((key) => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
    get() {
    const depend = getDepend(obj, key);
    depend.addDepend();
    return value;
    },
    set(newValue) {
    value = newValue;
    const depend = getDepend(obj, key);
    depend.notify();
    },
    });
    });
    return obj;
    }

    // 封装响应式函数
    function watchFn(fn) {
    activeFn = fn;
    fn();
    activeFn = null;
    }

    // 封装获取正确的depend ===========>WeakMap
    let wMap = new WeakMap();
    function getDepend(target, key) {
    let map = wMap.get(target);
    if (!map) {
    map = new Map();
    wMap.set(target, map);
    }

    let depend = map.get(key);
    if (!depend) {
    depend = new Depend();
    map.set(key, depend);
    }
    return depend;
    }

    let infoProxy = reactive(info);

    // name发生变化需要执行的逻辑
    watchFn(() => {
    console.log(infoProxy.name);
    });

    // age发生变化需要执行的逻辑
    watchFn(() => {
    console.log(infoProxy.age);
    console.log("-------------");
    });

    let test = {
    name: "test",
    };
    let testProxy = reactive(test);
    infoProxy.name = "zys";
    infoProxy.age = "1999";
    watchFn(() => {
    console.log(testProxy.name);
    });
    testProxy.name = "123";