8000 feat: auto wrapping for new properties · AlbertBrand/vue-function-api@69f4c8d · GitHub
[go: up one dir, main page]

Skip to content

Commit 69f4c8d

Browse files
committed
feat: auto wrapping for new properties
1 parent 750f309 commit 69f4c8d

File tree

6 files changed

+183
-64
lines changed

6 files changed

+183
-64
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ if (currentVue && typeof window !== 'undefined' && window.Vue) {
2626
}
2727

2828
export { plugin, Wrapper };
29+
export { set } from './reactivity';
2930
export * from './ts-api';
3031
export * from './functions/state';
3132
export * from './functions/lifecycle';

src/reactivity/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { observable } from './observable';
2+
export { set } from './set';

src/reactivity/observable.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ const AccessControlIdentifier = {};
88
const ObservableIdentifier = {};
99

1010
function setupAccessControl(target: AnyObject) {
11-
if (!isObject(target)) {
11+
if (!isObject(target) || isWrapper(target)) {
1212
return;
1313
}
14-
if (hasOwn(target, AccessControIdentifierlKey) && target.__ac__ === AccessControlIdentifier) {
14+
15+
if (
16+
hasOwn(target, AccessControIdentifierlKey) &&
17+
target[AccessControIdentifierlKey] === AccessControlIdentifier
18+
) {
1519
return;
1620
}
1721

@@ -22,57 +26,62 @@ function setupAccessControl(target: AnyObject) {
2226
}
2327
}
2428

25-
function defineAccessControl(target: AnyObject, key: any) {
29+
/**
30+
* Auto unwrapping when acccess property
31+
*/
32+
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
33+
let getter: (() => any) | undefined;
34+
let setter: ((x: any) => void) | undefined;
2635
const property = Object.getOwnPropertyDescriptor(target, key);
27-
if (!property) {
28-
return;
29-
}
30-
31-
if (property.configurable === false) {
32-
return;
36+
if (property) {
37+
if (property.configurable === false) {
38+
return;
39+
}
40+
getter = property.get;
41+
setter = property.set;
42+
val = target[key];
3343
}
3444

35-
let rawVal = target[key];
36-
let isValueWrapper = isWrapper(rawVal);
37-
38-
// we are sure that get and set exist.
39-
const getter = property.get!;
40-
const setter = property.set!;
41-
42-
setupAccessControl(target[key]);
45+
setupAccessControl(val);
4346
Object.defineProperty(target, key, {
4447
enumerable: true,
4548
configurable: true,
4649
get: function getterHandler() {
47-
if (isValueWrapper) {
48-
return rawVal.value;
50+
const value = getter ? getter.call(target) : val;
51+
if (isWrapper(value)) {
52+
return value.value;
4953
} else {
50-
return getter.call(target);
54+
return value;
5155
}
5256
},
5357
set: function setterHandler(newVal) {
54-
if (isValueWrapper) {
55-
rawVal.value = newVal;
56-
setupAccessControl(newVal);
57-
return;
58-
}
58+
if (getter && !setter) return;
5959

60-
if (isWrapper(newVal)) {
61-
isValueWrapper = true;
62-
rawVal = newVal;
63-
// trigger observer
60+
const value = getter ? getter.call(target) : val;
61+
if (isWrapper(value)) {
62+
if (isWrapper(newVal)) {
63+
val = newVal;
64+
} else {
65+
value.value = newVal;
66+
}
67+
} else if (setter) {
6468
setter.call(target, newVal);
65-
return;
69+
} else if (isWrapper(newVal)) {
70+
val = newVal;
6671
}
67-
68-
setter.call(target, newVal);
6972
setupAccessControl(newVal);
7073
},
7174
});
7275
}
7376

77+
export function isObservable(obj: any): boolean {
78+
return (
79+
hasOwn(obj, ObservableIdentifierKey) && obj[ObservableIdentifierKey] === ObservableIdentifier
80+
);
81+
}
82+
7483
export function observable<T = any>(obj: T): T {
75-
if (!isObject(obj)) {
84+
if (!isObject(obj) || isObservable(obj)) {
7685
return obj;
7786
}
7887

src/reactivity/set.ts

Lines changed: 64 additions & 0 deletions
E377
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { getCurrentVue } from '../runtimeContext';
2+
import { isArray } from '../utils';
3+
import { defineAccessControl } from './observable';
4+
5+
function isUndef(v: any): boolean {
6+
return v === undefined || v === null;
7+
}
8+
9+
function isPrimitive(value: any): boolean {
10+
return (
11+
typeof value === 'string' ||
12+
typeof value === 'number' ||
13+
// $flow-disable-line
14+
typeof value === 'symbol' ||
15+
typeof value === 'boolean'
16+
);
17+
}
18+
19+
function isValidArrayIndex(val: any): boolean {
20+
const n = parseFloat(String(val));
21+
return n >= 0 && Math.floor(n) === n && isFinite(val);
22+
}
23+
24+
/**
25+
* Set a property on an object. Adds the new property, triggers change
26+
* notification and intercept it's subsequent access if the property doesn't
27+
* already exist.
28+
*/
29+
export function set<T>(target: any, key: any, val: T): T {
30+
const Vue = getCurrentVue();
31+
const { warn, defineReactive } = Vue.util;
32+
if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) {
33+
warn(`Cannot set reactive property on undefined, null, or primitive value: ${target}`);
34+
}
35+
if (isArray(target) && isValidArrayIndex(key)) {
36+
target.length = Math.max(target.length, key);
37+
// IMPORTANT: define access control before trigger watcher
38+
defineAccessControl(target, key, val);
39+
target.splice(key, 1, val);
40+
return val;
41+
}
42+
if (key in target && !(key in Object.prototype)) {
43+
target[key] = val;
44+
return val;
45+
}
46+
const ob = (target as any).__ob__;
47+
if (target._isVue || (ob && ob.vmCount)) {
48+
process.env.NODE_ENV !== 'production' &&
49+
warn(
50+
'Avoid adding reactive properties to a Vue instance or its root $data ' +
51+
'at runtime - declare it upfront in the data option.'
52+
);
53+
return val;
54+
}
55+
if (!ob) {
56+
target[key] = val;
57+
return val;
58+
}
59+
defineReactive(ob.value, key, val);
60+
// IMPORTANT: define access control before trigger watcher
61+
defineAccessControl(target, key, val);
62+
ob.dep.notify();
63+
return val;
64+
}

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function assert(condition: any, msg: string) {
3636
}
3737

3838
export function isArray<T>(x: unknown): x is T[] {
39-
return toString(x) === '[object Array]';
39+
return Array.isArray(x);
4040
}
4141

4242
export function isObject(val: unknown): val is Record<any, any> {

test/functions/state.spec.js

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const Vue = require('vue/dist/vue.common.js');
2-
const { plugin, state, value, watch } = require('../../src');
2+
const { plugin, state, value, watch, set } = require('../../src');
33

44
Vue.use(plugin);
55

@@ -81,7 +81,7 @@ describe('reactivity/value', () => {
8181
expect(dummy).toBe(2);
8282
});
8383

84-
it('should work like a normal property when nested in an observable(1)', () => {
84+
it('should work like a normal property when nested in an observable(same ref)', () => {
8585
const a = value(1);
8686
const obj = state({
8787
a,
@@ -115,64 +115,108 @@ describe('reactivity/value', () => {
115115
expect(dummy3).toBe(3);
116116
});
117117

118-
it('should work like a normal property when nested in an observable(2)', () => {
118+
it('should work like a normal property when nested in an observable(different ref)', () => {
119119
const count = value(1);
120120
const count1 = value(1);
121121
const obj = state({
122122
a: count,
123123
b: {
124-
c: 1,
125-
d: [count1],
124+
c: count1,
126125
},
127126
});
128127

129-
// let dummy1;
128+
let dummy1;
130129
let dummy2;
131-
// let dummy3;
132130
watch(
133131
() => obj,
134132
() => {
135133
dummy1 = obj.a;
136134
dummy2 = obj.b.c;
137-
dummy3 = obj.b.d[0];
138135
},
139136
{ deep: true }
140137
);
141138
expect(dummy1).toBe(1);
142139
expect(dummy2).toBe(1);
143-
expect(dummy3).toBe(1);
144140
expect(obj.a).toBe(1);
145141
expect(obj.b.c).toBe(1);
146-
expect(obj.b.d[0]).toBe(1);
147-
148142
obj.a++;
149143
expect(dummy1).toBe(2);
150144
expect(dummy2).toBe(1);
151-
expect(dummy3).toBe(1);
152-
expect(obj.a).toBe(2);
153145
expect(count.value).toBe(2);
154146
expect(count1.value).toBe(1);
155-
156147
count.value++;
157-
count1.value++;
158-
obj.b.d[0]++;
159-
obj.b.c = 3;
160148
expect(dummy1).toBe(3);
161-
expect(dummy2).toBe(3);
162-
expect(dummy3).toBe(3);
163-
expect(obj.a).toBe(3);
164-
expect(count1.value).toBe(3);
165-
expect(obj.b.c).toBe(3);
166-
expect(obj.b.d[0]).toBe(3);
149+
expect(count.value).toBe(3);
150+
count1.value++;
151+
expect(dummy2).toBe(2);
152+
expect(count1.value).toBe(2);
153+
});
154+
155+
it('should work like a normal property when nested in an observable(wrapper overwrite)', () => {
156+
const obj = state({
157+
a: {
158+
b: 1,
159+
},
160+
});
167161

162+
let dummy;
163+
watch(
164+
() => obj,
165+
() => {
166+
dummy = obj.a.b;
167+
},
168+
{ deep: true, lazy: true }
169+
);
170+
expect(dummy).toBeUndefined();
168171
const wrapperC = value(1);
169-
obj.b.c = wrapperC;
170-
expect(dummy2).toBe(1);
171-
expect(obj.b.c).toBe(1);
172+
obj.a.b = wrapperC;
173+
expect(dummy).toBe(1);
174+
obj.a.b++;
175+
expect(dummy).toBe(2);
176+
});
172177

173-
obj.b.c++;
174-
expect(dummy2).toBe(2);
175-
expect(wrapperC.value).toBe(2);
176-
expect(obj.b.c).toBe(2);
178+
it('should work like a normal property when nested in an observable(new property of object)', () => {
179+
const count = value(1);
180+
const obj = state({
181+
a: {},
182+
b: [],
183+
});
184+
let dummy;
185+
watch(
186+
() => obj,
187+
() => {
188+
dummy = obj.a.foo;
189+
},
190+
{ deep: true }
191+
);
192+
expect(dummy).toBe(undefined);
193+
set(obj.a, 'foo', count);
194+
expect(dummy).toBe(1);
195+
count.value++;
196+
expect(dummy).toBe(2);
197+
obj.a.foo++;
198+
expect(dummy).toBe(3);
199+
});
200+
201+
it('should work like a normal property when nested in an observable(new property of array)', () => {
202+
const count = value(1);
203+
const obj = state({
204+
a: [],
205+
});
206+
let dummy;
207+
watch(
208+
() => obj,
209+
() => {
210+
dummy = obj.a[0];
211+
},
212+
{ deep: true }
213+
);
214+
expect(dummy).toBe(undefined);
215+
set(obj.a, 0, count);
216+
expect(dummy).toBe(1);
217+
count.value++;
218+
expect(dummy).toBe(2);
219+
obj.a[0]++;
220+
expect(dummy).toBe(3);
177221
});
178222
});

0 commit comments

Comments
 (0)
0