0%

vue-composition-api源码分析之reactive与ref

分析的是 @vue/composition-api 的代码,版本是 V1.0.0-rc.1
vue 的版本是 2.6.12

因为对于 reactive 和 ref 各自的行为和用法尚有不明之处,所以看一下源码,记录一下笔记

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├─scripts
├─src
│ ├─apis
│ ├─component
│ ├─reactivity
│ ├─types
│ └─utils
├─test
│ ├─apis
│ ├─helpers
│ ├─ssr
│ ├─types
│ └─v3
│ ├─reactivity
│ └─runtime-core
└─test-dts

这次主要关注 src/reactivity 和 src/apis 中的代码

reactivity/reactive.ts

reactive

reactive 和 ref 相关的方法在 reactivity 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @vue/composition-api/src/reactivity/reactive.ts
export function reactive<T extends object>(obj: T): UnwrapRef<T> {
if (
!(isPlainObject(obj) || isArray(obj)) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
return obj as any;
}

const observed = observe(obj);
setupAccessControl(observed);
return observed as UnwrapRef<T>;
}

然后是看 observe 是啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @vue/composition-api/src/reactivity/reactive.ts
function observe(obj) {
var Vue = getRegisteredVueOrDefault();
var observed;
if (Vue.observable) {
observed = Vue.observable(obj);
} else {
var vm = defineComponentInstance(Vue, {
data: {
$$state: obj,
},
});
observed = vm._data.$$state;
}
// in SSR, there is no __ob__. Mock for reactivity check
if (!hasOwn(observed, "__ob__")) {
def(observed, "__ob__", mockObserver(observed));
}
return observed;
}

如果 Vue 上有 Vue.observe,使用 Vue.observe

1
2
3
4
5
// vue/src/core/global-api/index.js
Vue.observable = function (obj) {
observe(obj);
return obj;
};

Vue.observe 直接返回了传入的对象,也就是说,reactive 的作用只是给对象增加响应式,返回的值是对象本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vue/src/core/observer/index.js
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return;
}
var ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}

如果之前没有被 observe 过,使用 new Observer(value) 添加响应式属性,否则什么都不做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vue/src/core/observer/index.js
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};

可以看到这个构造方法给对象增加了 __ob__属性。

那么看以下代码

1
2
3
4
5
let a = {
a: 100,
);
let b = reactive(a);
reactive(b)

这么使用 reactive 是没问题的,同时 a,b 这里指向的时同一个响应式对象,reactive(b) 这句实际上没有任何作用

显然我们不能重新给 a 或 b 赋值,因为响应性是赋予在 a,b 指向的对象上的

然后看下 setupAccessControl 干了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @vue/composition-api/src/reactivity/reactive.ts
function setupAccessControl(target: AnyObject): void {
if (
!isPlainObject(target) ||
isRaw(target) ||
Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target) ||
accessModifiedSet.has(target)
)
return;

accessModifiedSet.set(target, true);

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

accessModifiedSet 是一个 weakmap,如果之前对对象设置过,就不再设置了
对对象的每一个属性调用 defineAccessControl

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
// @vue/composition-api/src/reactivity/reactive.ts
/**
* Auto unwrapping when access property
*/
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
if (key === "__ob__") return;
if (isRaw(target[key])) return;

let getter: (() => any) | undefined;
let setter: ((x: any) => void) | undefined;
const property = Object.getOwnPropertyDescriptor(target, key);
if (property) {
if (property.configurable === false) {
return;
}
getter = property.get;
setter = property.set;
if (
(!getter || setter) /* not only have getter */ &&
arguments.length === 2
) {
val = target[key];
}
}

// 如果 val 是一个对象 递归调用 defineAccessControl
setupAccessControl(val);
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(target) : val;
// if the key is equal to RefKey, skip the unwrap logic
if (key !== RefKey && isRef(value)) {
return value.value;
} else {
return value;
}
},
set: function setterHandler(newVal) {
if (getter && !setter) return;

const value = getter ? getter.call(target) : val;
// If the key is equal to RefKey, skip the unwrap logic
// If and only if "value" is ref and "newVal" is not a ref,
// the assignment should be proxied to "value" ref.
if (key !== RefKey && isRef(value) && !isRef(newVal)) {
value.value = newVal;
} else if (setter) {
setter.call(target, newVal);
} else {
val = newVal;
}
setupAccessControl(newVal);
},
});
}

function setupAccessControl(target: AnyObject): void {
if (
!isPlainObject(target) ||
isRaw(target) ||
Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target) ||
accessModifiedSet.has(target)
)
return;

accessModifiedSet.set(target, true);

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

如果 val 是一个对象 递归调用 defineAccessControl
defineAccessControl 的作用是 reactive 对象在读写属性时,如果属性是一个 Ref 对象,可以自动拆装箱

shallowReactive

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
// @vue/composition-api/src/reactivity/reactive.ts
export function shallowReactive(obj: any): any {
if (
!(isPlainObject(obj) || isArray(obj)) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
return obj as any;
}

const observed = observe({});
setupAccessControl(observed);

const ob = (observed as any).__ob__;

for (const key of Object.keys(obj)) {
let val = obj[key];
let getter: (() => any) | undefined;
let setter: ((x: any) => void) | undefined;
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property) {
if (property.configurable === false) {
continue;
}
getter = property.get;
setter = property.set;
}

Object.defineProperty(observed, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(obj) : val;
ob.dep?.depend();
return value;
},
set: function setterHandler(newVal) {
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
ob.dep?.notify();
},
});
}
return observed;
}

只对响应式对象的属性手动设置响应式并管理依赖收集,如果属性是对象,不会深度添加响应式

isReactive

1
2
3
4
// @vue/composition-api/src/reactivity/reactive.ts
export function isReactive(obj: any): boolean {
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__);
}

就是看对象是否有 __ob__ 属性并且没有使用 markRaw 标记

markRaw && isRaw && toRaw

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
// @vue/composition-api/src/reactivity/reactive.ts
export function markRaw<T extends object>(obj: T): T {
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj;
}

// set the vue observable flag at obj
const ob = createObserver();
ob.__raw__ = true;
def(obj, "__ob__", ob);

// mark as Raw
rawSet.set(obj, true);

return obj;
}
export function isRaw(obj: any): boolean {
return Boolean(obj?.__ob__ && obj.__ob__?.__raw__);
}
export function toRaw<T>(observed: T): T {
if (isRaw(observed) || !Object.isExtensible(observed)) {
return observed;
}

return (observed as any)?.__ob__?.value || observed;
}

markRaw 给目标添加了 vue observable 的 flag,这样 Vue 应该就不会再给对象添加响应式属性了
rawSet 也是一个 weakmap
isRaw 通过 markRaw 设置的 __ob__.__raw__ flag 来判断
toRaw 是获取响应式对象的原本对象
这里 和 vue3 实现的有些差异,通过之前我们知道 reactive 是在原对象上增加响应式属性__ob__的,而 vue3 是通过 proxy 实现的,所以 toRaw 拿到的是被代理的原始对象,而这里这个 toRaw 有点意义不明

reactivity/ref.ts

这个文件上来就是一堆类型定义,看的我人晕掉了。。

ref && isRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @vue/composition-api/src/reactivity/ref.ts
export const RefKey = "composition-api.refKey";

export function ref<T extends object>(
raw: T
): T extends Ref ? T : Ref<UnwrapRef<T>>;
export function ref<T>(raw: T): Ref<UnwrapRef<T>>;
export function ref<T = any>(): Ref<T | undefined>;
export function ref(raw?: unknown) {
if (isRef(raw)) {
return raw;
}

const value = reactive({ [RefKey]: raw });
return createRef({
get: () => value[RefKey] as any,
set: (v) => ((value[RefKey] as any) = v),
});
}

好家伙,上来就是三个重载,然后 Ref<UnwrapRef<T>>这玩意好绕,先不管了
如果传入的对象已经是 Ref 对象,直接返回,否则通过 reactive 创建一个响应式对象,然后返回通过 createRef 创建的对象

1
2
3
4
// @vue/composition-api/src/reactivity/ref.ts
export function isRef<T>(value: any): value is Ref<T> {
return value instanceof RefImpl;
}

通过 instanceOf 判断是否是 Ref 对象是

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
// @vue/composition-api/src/reactivity/ref.ts
export function createRef<T>(options: RefOption<T>, readonly = false) {
const r = new RefImpl<T>(options);
// seal the ref, this could prevent ref from being observed
// It's safe to seal the ref, since we really shouldn't extend it.
// related issues: #79
const sealed = Object.seal(r);

readonlySet.set(sealed, true);
return sealed;
}
class RefImpl<T> implements Ref<T> {
readonly [_refBrand]!: true;
public value!: T;
constructor({ get, set }: RefOption<T>) {
proxy(this, "value", {
get,
set,
});
}
}
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noopFn,
set: noopFn,
};

export function proxy(
target: any,
key: string,
{ get, set }: { get?: Function; set?: Function }
) {
sharedPropertyDefinition.get = get || noopFn;
sharedPropertyDefinition.set = set || noopFn;
Object.defineProperty(target, key, sharedPropertyDefinition);
}

createRef 就是创建一个 RefImpl 对象,然后代理了其中 value 属性的 getter 和 setter
Object.seal 让一个对象密封,并返回被密封后的对象。密封对象是指那些不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值的对象。

toRef && toRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @vue/composition-api/src/reactivity/ref.ts
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
const v = object[key];
if (isRef<T[K]>(v)) return v;

return createRef({
get: () => object[key],
set: (v) => (object[key] = v),
});
}
export function toRefs<T extends Data = Data>(obj: T): ToRefs<T> {
if (!isPlainObject(obj)) return obj as any;

const ret: any = {};
for (const key in obj) {
ret[key] = toRef(obj, key);
}

return ret;
}

把对象的某个属性变成 Ref 类型
如果已经是 Ref 类型了,直接返回,否则返回通过 createRef 创建的对象
需要注意传入的 object 必须是一个响应式对象,否则返回的 Ref 是不具有响应式特性的,因为无法进行依赖收集相关的操作。

unref

1
2
3
4
// @vue/composition-api/src/reactivity/ref.ts
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref;
}

返回 Ref 的 value 属性

shallowRef

1
2
3
4
5
6
7
8
9
10
11
// @vue/composition-api/src/reactivity/ref.ts
export function shallowRef(raw?: unknown) {
if (isRef(raw)) {
return raw;
}
const value = shallowReactive({ [RefKey]: raw });
return createRef({
get: () => value[RefKey] as any,
set: (v) => ((value[RefKey] as any) = v),
});
}

很好理解,这里的 reactive 对象是通过 shallowReactive 创建的

customRef

1
2
3
4
5
6
7
8
9
10
11
12
// @vue/composition-api/src/reactivity/ref.ts
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
const version = ref(0);
return createRef(
factory(
() => void version.value,
() => {
++version.value;
}
)
);
}

允许传入一个工厂方法,属于比较高级的应用,略过

triggerRef

1
2
3
4
5
6
// @vue/composition-api/src/reactivity/ref.ts
export function triggerRef(value: any) {
if (!isRef(value)) return;

value.value = value.value;
}

vue3 文档上的例子,手动触发响应式侦测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const shallow = shallowRef({
greet: "Hello, world",
});

// Logs "Hello, world" once for the first run-through
watchEffect(() => {
console.log(shallow.value.greet);
});

// This won't trigger the effect because the ref is shallow
shallow.value.greet = "Hello, universe";

// Logs "Hello, universe"
triggerRef(shallow);

proxyRefs

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
// @vue/composition-api/src/reactivity/ref.ts
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
if (isReactive(objectWithRefs)) {
return objectWithRefs as ShallowUnwrapRef<T>;
}
const value: Record<string, any> = reactive({ [RefKey]: objectWithRefs });

for (const key of Object.keys(objectWithRefs)) {
proxy(value, key, {
get() {
if (isRef(value[key])) {
return value[key].value;
}
return value[key];
},
set(v: unknown) {
if (isRef(value[key])) {
return (value[key].value = unref(v));
}
value[key] = unref(v);
},
});
}

return value as ShallowUnwrapRef<T>;
}

看了一下,是 vue3 的新特性方法,这里也跟进了
如果 objectWithRefs 的属性是 Ref 的话,自动进行拆装箱