8000 feat: Call `setup` before state options · e3d/vue-function-api@1d532fe · GitHub
[go: up one dir, main page]

Skip to content

Commit 1d532fe

Browse files
committed
feat: Call setup before state options
BREAKING CHANGE: The order of `setup()` call has changed `setup()` will be called before data, computed and method options are resolved, it was called after these options previously. You can access values returned from `setup()` on `this` in data, computed and method options, but not the other way around.
1 parent 69f4c8d commit 1d532fe

File tree

9 files changed

+261
-146
lines changed

9 files changed

+261
-146
lines changed

src/functions/watch.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,17 @@ function createMuiltSourceWatcher<T>(
219219
return stop;
220220
}
221221

222-
export function watch<T>(
222+
export function watch<T = any>(
223223
source: watchedValue<T>,
224224
cb: watcherCallBack<T>,
225225
options?: Partial<WatcherOption>
226226
): () => void;
227-
export function watch<T>(
227+
export function watch<T = any>(
228228
source: Array<watchedValue<T>>,
229229
cb: watcherCallBack<T[]>,
230230
options?: Partial<WatcherOption>
231231
): () => void;
232-
export function watch<T>(
232+
export function watch<T = any>(
233233
source: watchedValue<T> | Array<watchedValue<T>>,
234234
cb: watcherCallBack<T> | watcherCallBack<T[]>,
235235
options: Partial<WatcherOption> = {}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { mixin } from './setup';
88
declare module 'vue/types/options' {
99
interface ComponentOptions<V extends Vue> {
1010
setup?: (
11-
this: undefined,
11+
this: void,
1212
props: { [x: string]: any },
1313
context: SetupContext
1414
) => object | null | undefined | void;

src/setup.ts

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,104 @@
1-
import { VueConstructor } from 'vue';
1+
import VueInstance, { VueConstructor } from 'vue';
22
import { SetupContext } from './types/vue';
33
import { isWrapper } from './wrappers';
4+
import { SetupHookEvent } from './symbols';
45
import { setCurrentVM } from './runtimeContext';
5-
import { isPlainObject, assert, proxy } from './utils';
6+
import { isPlainObject, assert, proxy, noopFn } from './utils';
67
import { value } from './functions/state';
8+
import { watch } from './functions/watch';
9+
10+
let disableSetup = false;
11+
12+
// `cb` should be called right after props get resolved
13+
function waitPropsResolved(vm: VueInstance, cb: (v: VueInstance, props: Record<any, any>) => void) {
14+
const safeRunCb = (props: Record<any, any>) => {
15+
// Running `cb` under the scope of a dep.Target, otherwise the `Observable`
16+
// in `cb` will be unexpectedly colleced by the current dep.Target.
17+
const dispose = watch(
18+
() => {
19+
cb(vm, props);
20+
},
21+
noopFn,
22+
{ lazy: false, deep: false, flush: 'sync' }
23+
);
24+
dispose();
25+
};
26+
27+
const opts = vm.$options;
28+
let methods = opts.methods;
29+
30+
if (!methods) {
31+
opts.methods = { [SetupHookEvent]: noopFn };
32+
// This will get invoked when assigning to `SetupHookEvent` property of vm.
33+
vm.$once(SetupHookEvent, () => {
34+
// restore `opts` object
35+
delete opts.methods;
36+
safeRunCb(vm.$props);
37+
});
38+
return;
39+
}
40+
41+
// Find the first method will re resovled.
42+
// The order will be stable, since we never modify the `methods` object.
43+
let firstMedthodName: string | undefined;
44+
for (const key in methods) {
45+
firstMedthodName = key;
46+
break;
47+
}
48+
49+
// `methods` is an empty object
50+
if (!firstMedthodName) {
51+
methods[SetupHookEvent] = noopFn;
52+
vm.$once(SetupHookEvent, () => {
53+
// restore `methods` object
54+
delete methods![SetupHookEvent];
55+
safeRunCb(vm.$props);
56+
});
57+
return;
58+
}
59+
60+
proxy(vm, firstMedthodName, {
61+
set(val: any) {
62+
safeRunCb(vm.$props);
63+
64+
// restore `firstMedthodName` to a noraml property
65+
Object.defineProperty(vm, firstMedthodName!, {
66+
configurable: true,
67+
enumerable: true,
68+
writable: true,
69+
value: val,
70+
});
71+
},
72+
});
73+
}
774

875
export function mixin(Vue: VueConstructor) {
76+
// We define the setup hook on prototype,
77+
// which avoids Object.defineProperty calls for each instance created.
78+
proxy(Vue.prototype, SetupHookEvent, {
79+
get() {
80+
return 'hook';
81+
},
82+
set(this: VueInstance) {
83+
this.$emit(SetupHookEvent);
84+
},
85+
});
86+
987
Vue.mixin({
10-
created: functionApiInit,
88+
beforeCreate: functionApiInit,
1189
});
1290

1391
/**
1492
* Vuex init hook, injected into each instances init hooks list.
1593
*/
16-
function functionApiInit(this: any) {
94+
function functionApiInit(this: VueInstance) {
1795
const vm = this;
1896
const { setup } = vm.$options;
19-
if (!setup) {
97+
98+
if (disableSetup || !setup) {
2099
return;
21100
}
101+
22102
if (typeof setup !== 'function') {
23103
if (process.env.NODE_ENV !== 'production') {
24104
Vue.util.warn(
@@ -29,11 +109,16 @@ export function mixin(Vue: VueConstructor) {
29109
return;
30110
}
31111

112+
waitPropsResolved(vm, initSetup);
113+
}
114+
115+
function initSetup(vm: VueInstance, props: Record<any, any> = {}) {
116+
const setup = vm.$options.setup!;
117+
const ctx = createSetupContext(vm);
32118
let binding: any;
33119
setCurrentVM(vm);
34-
const ctx = createContext(vm);
35120
try {
36-
binding = setup(vm.$props || {}, ctx);
121+
binding = setup(props, ctx);
37122
} catch (err) {
38123
if (process.env.NODE_ENV !== 'production') {
39124
Vue.util.warn(`there is an error occuring in "setup"`, vm);
@@ -67,43 +152,27 @@ export function mixin(Vue: VueConstructor) {
67152
});
68153
}
69154

70-
function createContext(vm: any): SetupContext {
155+
function createSetupContext(vm: VueInstance & { [x: string]: any }): SetupContext {
71156
const ctx = {} as SetupContext;
72-
const props = [
73-
// 'el', // has workaround
74-
// 'options',
75-
'parent', // confirmed in rfc
76-
'root', // confirmed in rfc
77-
// 'children', // very likely
78-
'refs', // confirmed in rfc
79-
'slots', // confirmed in rfc
80-
// 'scopedSlots', // has workaround
81-
// 'isServer',
82-
// 'ssrContext',
83-
// 'vnode',
84-
'attrs', // confirmed in rfc
85-
// 'listeners', // very likely
86-
];
87-
const methodWithoutReturn = [
88-
// 'on', // very likely
89-
// 'once', // very likely
90-
// 'off', // very likely
91-
'emit', // confirmed in rfc
92-
// 'forceUpdate',
93-
// 'destroy'
94-
];
157+
const props = ['parent', 'root', 'refs', 'slots', 'attrs'];
158+
const methodReturnVoid = ['emit'];
95159
props.forEach(key => {
96-
proxy(ctx, key, () => vm[`$${key}`], function() {
97-
Vue.util.warn(`Cannot assign to '${key}' because it is a read-only property`, vm);
160+
proxy(ctx, key, {
161+
get: () => vm[`$${key}`],
162+
set() {
163+
Vue.util.warn(`Cannot assign to '${key}' because it is a read-only property`, vm);
164+
},
98165
});
99166
});
100-
methodWithoutReturn.forEach(key =>
101-
proxy(ctx, key, () => {
102-
const vmKey = `$${key}`;
103-
return (...args: any[]) => {
104-
const fn: Function = vm[vmKey];
105-
fn.apply(vm, args);
106-
};
167+
methodReturnVoid.forEach(key =>
168+
proxy(ctx, key, {
169+
get() {
170+
const vmKey = `$${key}`;
171+
return (...args: any[]) => {
172+
const fn: Function = vm[vmKey];
173+
fn.apply(vm, args);
174+
};
175+
},
107176
})
108177
);
109178
if (process.env.NODE_ENV === 'test') {

src/symbols.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue');
88
export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue');
99
export const AccessControIdentifierlKey = createSymbol('vfa.key.accessControIdentifier');
1010
export const ObservableIdentifierKey = createSymbol('vfa.key.observableIdentifier');
11+
// event name should be a string
12+
export const SetupHookEvent = 'vfa.key.setupHookEvent';

src/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const sharedPropertyDefinition = {
1111
set: noopFn,
1212
};
1313

14-
export function proxy(target: any, key: string, getter: Function, setter?: Function) {
15-
sharedPropertyDefinition.get = getter;
16-
sharedPropertyDefinition.set = setter || noopFn;
14+
export function proxy(target: any, key: string, { get, set }: { get?: Function; set?: Function }) {
15+
sharedPropertyDefinition.get = get || noopFn;
16+
sharedPropertyDefinition.set = set || noopFn;
1717
Object.defineProperty(target, key, sharedPropertyDefinition);
1818
}
1919

src/wrappers/AbstractWrapper.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,23 @@ export default abstract class AbstractWrapper<V> {
1212
def(this, '_propName', propName);
1313

1414
const props = vm.$options.props;
15-
const methods = vm.$options.methods;
16-
const computed = vm.$options.computed;
1715
const warn = getCurrentVue().util.warn;
18-
if (!(propName in vm)) {
19-
proxy(
20-
vm,
21-
propName,
22-
() => this.value,
23-
(val: any) => {
16+
if (!(propName in vm) && !(props && hasOwn(props, propName))) {
17+
proxy(vm, propName, {
18+
get: () => this.value,
19+
set: (val: any) => {
2420
this.value = val;
25-
}
26-
);
21+
},
22+
});
2723
if (process.env.NODE_ENV !== 'production') {
28-
this.exposeToDevtool();
24+
// after data has resolved, expose bindings to vm._data.
25+
vm.$nextTick(() => {
26+
this.exposeToDevtool();
27+
});
2928
}
3029
} else if (process.env.NODE_ENV !== 'production') {
31-
if (hasOwn(vm.$data, propName)) {
32-
warn(`The setup binding property "${propName}" is already declared as a data.`, vm);
33-
} else if (props && hasOwn(props, propName)) {
30+
if (props && hasOwn(props, propName)) {
3431
warn(`The setup binding property "${propName}" is already declared as a prop.`, vm);
35-
} else if (methods && hasOwn(methods, propName)) {
36-
warn(`The setup binding property "${propName}" is already declared as a method.`, vm);
37-
} else if (computed && propName in computed) {
38-
warn(`The setup binding property "${propName}" is already declared as a computed.`, vm);
3932
} else {
4033
warn(`The setup binding property "${propName}" is already declared.`, vm);
4134
}

src/wrappers/ComputedWrapper.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ export default class ComputedWrapper<V> extends AbstractWrapper<V> {
4343
vm.$options.computed = {};
4444
}
4545

46-
proxy(vm.$options.computed, name, () => ({
47-
get: () => this.value,
48-
set: (val: any) => {
49-
this.value = val;
50-
},
51-
}));
46+
proxy(vm.$options.computed, name, {
47+
get: () => ({
48+
get: () => this.value,
49+
set: (val: any) => {
50+
this.value = val;
51+
},
52+
}),
53+
});
5254
}
5355
}
5456
}

src/wrappers/ValueWrapper.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ export default class ValueWrapper<V> extends AbstractWrapper<V> {
2525
if (process.env.NODE_ENV !== 'production') {
2626
const vm = this._vm!;
2727
const name = this._propName!;
28-
proxy(
29-
vm._data,
30-
name,
31-
() => this.value,
32-
(val: any) => {
28+
proxy(vm._data, name, {
29+
get: () => this.value,
30+
set: (val: any) => {
3331
this.value = val;
34-
}
35-
);
32+
},
33+
});
3634
}
3735
}
3836
}

0 commit comments

Comments
 (0)
0