vue3源码学习-3-实现reactivity

前言

经过前面的环境搭建以及项目构建,完成了基础的项目框架,下面学习实现 vue3 的 reactivity。

观察官方如何使用

首先修改.npmrc文件

1
2
# 解决一个问题 例如vue中有个依赖abc ,那么我们安装了vue就可以直接用abd,有一天vue不依赖abc了,那么你用abc就出错了,未来让这种幽灵依赖以后不出错,就在这里配置羞耻提升
shamefully-hoist = true

我们在 vue3-plan 上安装 vue3

1
pnpm install vue -w

这个时候发现node_moules中 vue 的依赖被展开了放在根目录上,在packages/reactivity/index.html上引入 vue 官方的reactivity

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- <script src="./dist/reactivity.global.js"></script> -->
<div id="app"></div>

<!-- 官方的 -->
<script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>

<!-- 自己实现的 -->
<!-- <script src="./dist/reactivity.global.js"></script> -->
<script>
// effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
// reactive 将数据变成响应式 相当于proxy
// shallowRactive,readonly,shallowReadonly
const { effect, reactive } = VueReactivity;

const state = reactive({ name: "david", age: 12, address: { num: 567 } });

//set 和 map 也可以劫持

effect(() => {
document.getElementById(
"app"
).innerHTML = `${state.name}今年${state.age}`;
});
setTimeout(() => {
state.age = 13;
}, 1000);
</script>
</body>
</html>

通过上面的实验观察发现通过reactive包裹之后的对象,能被监听到变化,然后effect通过监听到变化而触发回调函数,从而打印出上面到语句。并且reactive是能深层检测到对象的改变,当你修改了address里面的 num 值时也能被监听到变化,这得益于 vue3 采用到proxyshallowRactiveshallowReadonly如名字,只能监听到表层,以为深处到属性并未做包装。

vue3 对比 vue2 的变化

  • 在 Vue2 的时候采用defineProperty来进行数据的劫持,需要对属性进行重写gettersetter性能差。
  • 当新增属性和删除属性式就无法监听变化,需要通过$set$delete实现。
  • 数组不采用 defineProperty 来进行劫持(浪费性能,对所有索引进行劫持会造成性能的浪费)需要对数组单独进行处理。

编写自己的响应式

首先引入的 JS 文件的 html,从官方的引入链接改成引入自己的链接

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- <script src="./dist/reactivity.global.js"></script> -->
<div id="app"></div>

<!-- 官方的 -->
<!-- <script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->

<!-- 自己实现的 -->
<script src="./dist/reactivity.global.js"></script>
<script>
// effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
// reactive 将数据变成响应式 相当于proxy
// shallowRactive,readonly,shallowReadonly
const { effect, reactive } = VueReactivity;

const state = reactive({ name: "david", age: 12, address: { num: 567 } });

//set 和 map 也可以劫持

effect(() => {
document.getElementById(
"app"
).innerHTML = `${state.name}今年${state.age}`;
});
setTimeout(() => {
state.age = 13;
}, 1000);
</script>
</body>
</html>

为功能划分文件

reactivity/src/下新建 effect.ts 和 reactive.ts 文件,对应上面 html 的 2 个功能。

1
2
# reactive.ts
export function reactive() {}
1
2
# effect.ts
export function effect() {}

同时在 index.ts 中抛出这 2 个函数

1
2
3
4
import { effect } from "./effect";
import { reactive } from "./reactive";

export { effect, reactive };

这样 html 中引入编译好的 JS 文件就能获取这 2 个函数了。

  1. 编写 reactive 功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { isObject } from "@vue/shared";

    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
    if (!isObject(target)) {
    return;
    }

    // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
    const proxy = new Proxy(target, {
    get(target, key, recevier) {
    return target[key];
    },
    set(target, value, key, recevier) {
    target[key] = value;
    return true;
    },
    });
    return proxy;
    }

    上面定义个了 proxy 代理对象,但是为啥不能如上图编写。看下面的解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 代码省略,和上面的一致
    let target = {
    name : "java",
    get alias(){ //属性访问器写法 es5
    console.log(this); // this { name: 'java', alias: [Getter] }
    return this.name;
    }
    }
    const proxy = new Proxy(target, {
    get(target, key, recevier) {
    // return target[key] ;

    // 这里控制台会打印出 alias name
    console.log(key)
    return Reflect.get(target,key,recevier)
    },
    set(target, value, key, recevier) {
    target[key] = value
    return true
    },
    })
    proxy.alias
    // 通过proxy.alias,触发了get,然后return target[key],这个时候访问的是原对象target,traget又访问alias,alias访问了name,但是这个this是源对象,监控不到name,所以引入Reflect(反射)对象,这样访问alias,就回去代理对象上取值,这个时候this就变成了代理对象,那么this.name就又走一次get,这样name就被监控到。 recevier的作用是改变this指向

    经过上面的修改,初步得到了一个代理对象的方法。此时如果用户在使用上面的代码的时候,他是这么写的

    1
    2
    3
    4
    5
    const { effect, reactive } = VueReactivity;
    let target = { name: "david", age: 13, address: { num: 134 } };
    let p1 = reactive(target);
    let p2 = reactive(target);
    console.log(p1 === p2); //打印出false,因为每次都new了一个新的Porxy();

    那么实际上这 2 个应该使用一个对象的,为此我们修改一下上面的代码,增加缓存设置,这里用上了WeakMap。弱链接 Map,好处在于 key 为 null 自动清空对应映射关系,其二是 key 只能为对象。修改上面的代码为

    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 { isObject } from '@vue/shared'

    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
    import { isObject } from '@vue/shared'
    const reactiveMap = new WeakMap() // key只能是对象
    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
    if (!isObject(target)) {
    return
    }

    let existingProxy = reactiveMap.get(target)
    if (existingProxy) {
    return existingProxy
    }
    // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
    const proxy = new Proxy(target, {
    get(target, key, recevier) {
    // return target[key]
    console.log(key)
    return Reflect.get(target, key, recevier)
    },
    set(target, value, key, recevier) {
    // target[key] = value
    // return true
    return Reflect.set(target, key, value, recevier)
    },
    })
    reactiveMap.set(target, proxy)
    return proxy
    }

    这个时候 reactive 就有了同一个对象代理多次,返回同一个代理。现在又有个新需求,如果代理再一次被代理,那应该返回代理,而不是代理的代理对象。

    1
    2
    3
    4
    let target = { name: "david", age: 12, address: { num: 567 } };
    const state = reactive(target);
    const state2 = reactive(state);
    console.log(state === state2); //false

    那么怎么让判断为 true 呢,早期的处理方式是,WeakMap,正方向存一次,反方向存一次就像
    target -> proxy
    proxy -> target
    最新的处理方法是定一个枚举变量。当你你传入的是 proxy 的时候,可以看一下时候代理过,如果有,那么他一定走到了 get 方法,并且我们访问了ReactiveFlags.IS_RECEIVE,那么就表示这个是被代理过的,就直接返回 target。

    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
    import { isObject } from "@vue/shared";
    const reactiveMap = new WeakMap(); // key只能是对象
    const enum ReactiveFlags {
    IS_RECEIVE = `__v_isReactive`,
    }
    // 将数据转化成响应式数据,只能做对象的代理
    // 同一个对象被代理多次返回同一个代理
    // 代理再次被代理,返回原代理
    export function reactive(target: object) {
    if (!isObject(target)) {
    return;
    }
    //
    if (target[ReactiveFlags.IS_RECEIVE]) {
    return target;
    }

    let existingProxy = reactiveMap.get(target);
    if (existingProxy) {
    return existingProxy;
    }
    // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
    const proxy = new Proxy(target, {
    // 第一次是普通对象,只是代理,在取值的时候会调用get
    // 下一次你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE,
    // 那么就表示这个是被代理过的,就直接返回 target
    get(target, key, recevier) {
    // return target[key]
    if (key == ReactiveFlags.IS_RECEIVE) {
    return true;
    }
    console.log(key);
    return Reflect.get(target, key, recevier);
    },
    set(target, value, key, recevier) {
    // target[key] = value
    // return true
    return Reflect.set(target, key, value, recevier);
    },
    });
    reactiveMap.set(target, proxy);
    return proxy;
    }
1
git:[@github/MicroMatrixOrg/vue3-plan/tree/watch]