vue3源码学习-3-实现reactivity
前言
经过前面的环境搭建以及项目构建,完成了基础的项目框架,下面学习实现 vue3 的 reactivity。
观察官方如何使用
首先修改.npmrc
文件
1 | # 解决一个问题 例如vue中有个依赖abc ,那么我们安装了vue就可以直接用abd,有一天vue不依赖abc了,那么你用abc就出错了,未来让这种幽灵依赖以后不出错,就在这里配置羞耻提升 |
我们在 vue3-plan 上安装 vue3
1 | pnpm install vue -w |
这个时候发现node_moules
中 vue 的依赖被展开了放在根目录上,在packages/reactivity/index.html
上引入 vue 官方的reactivity
。
1 |
|
通过上面的实验观察发现通过reactive
包裹之后的对象,能被监听到变化,然后effect
通过监听到变化而触发回调函数,从而打印出上面到语句。并且reactive
是能深层检测到对象的改变,当你修改了address
里面的 num 值时也能被监听到变化,这得益于 vue3 采用到proxy
。shallowRactive
和shallowReadonly
如名字,只能监听到表层,以为深处到属性并未做包装。
vue3 对比 vue2 的变化
- 在 Vue2 的时候采用
defineProperty
来进行数据的劫持,需要对属性进行重写getter
和setter
性能差。 - 当新增属性和删除属性式就无法监听变化,需要通过
$set
、$delete
实现。 - 数组不采用 defineProperty 来进行劫持(浪费性能,对所有索引进行劫持会造成性能的浪费)需要对数组单独进行处理。
编写自己的响应式
首先引入的 JS 文件的 html,从官方的引入链接改成引入自己的链接
1 |
|
为功能划分文件
在reactivity/src/
下新建 effect.ts 和 reactive.ts 文件,对应上面 html 的 2 个功能。
1 | # reactive.ts |
1 | # effect.ts |
同时在 index.ts 中抛出这 2 个函数
1 | import { effect } from "./effect"; |
这样 html 中引入编译好的 JS 文件就能获取这 2 个函数了。
编写 reactive 功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { 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
5const { 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
32import { 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
4let 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
43import { 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] |