8000 fix: multisource-watch callback is called repeatedly · cuulee/vue-function-api@185a2cc · GitHub
[go: up one dir, main page]

Skip to content

Commit 185a2cc

Browse files
committed
fix: multisource-watch callback is called repeatedly
1 parent 4c55a58 commit 185a2cc

File tree

3 files changed

+182
-104
lines changed

3 files changed

+182
-104
lines changed

src/functions/watch.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import { VueConstructor } from 'vue';
1+
import Vue, { VueConstructor } from 'vue';
22
import { Wrapper } from '../wrappers';
33
import { isArray, assert } from '../utils';
44
import { isWrapper } from '../helper';
55
import { getCurrentVM, getCurrentVue } from '../runtimeContext';
66
import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols';
77

8+
const initValue = {};
9+
type InitValue = typeof initValue;
810
type watcherCallBack<T> = (newVal: T, oldVal: T) => void;
911
type watchedValue<T> = Wrapper<T> | (() => T);
10-
type FlushMode = 'pre' | 'post' | 'sync' | 'auto';
12+
type FlushMode = 'pre' | 'post' | 'sync';
1113
interface WatcherOption {
1214
lazy: boolean;
1315
deep: boolean;
1416
flush: FlushMode;
1517
}
1618
interface WatcherContext<T> {
17-
value: T;
18-
oldValue: T;
19+
getter: () => T;
20+
value: T | InitValue;
21+
oldValue: T | InitValue;
1922
watcherStopHandle: Function;
2023
}
2124

25+
let fallbackVM: Vue;
26+
2227
function hasWatchEnv(vm: any) {
2328
return vm[WatcherPreFlushQueueKey] !== undefined;
2429
}
@@ -66,7 +71,6 @@ function flushWatcherCallback(vm: any, fn: Function, mode: FlushMode) {
6671
fallbackFlush();
6772
vm[WatcherPostFlushQueueKey].push(fn);
6873
break;
69-
case 'auto':
7074
case 'sync':
7175
fn();
7276
break;
@@ -131,15 +135,15 @@ function createMuiltSourceWatcher<T>(
131135
): () => void {
132136
let execCallbackAfterNumRun: false | number = options.lazy ? false : sources.length;
133137
let pendingCallback = false;
134-
const watcherContext: WatcherContext<T>[] = [];
138+
const watcherContext: Array<WatcherContext<T>> = [];
135139

136140
function execCallback() {
137141
cb.apply(
138142
vm,
139143
watcherContext.reduce<[T[], T[]]>(
140144
(acc, ctx) => {
141-
acc[0].push(ctx.value);
142-
acc[1].push(ctx.oldValue);
145+
acc[0].push((ctx.value === initValue ? ctx.getter() : ctx.value) as T);
146+
acc[1].push((ctx.oldValue === initValue ? undefined : ctx.oldValue) as T);
143147
return acc;
144148
},
145149
[[], []]
@@ -166,14 +170,16 @@ function createMuiltSourceWatcher<T>(
166170
const flush = () => {
167171
if (!pendingCallback) {
168172
pendingCallback = true;
169-
flushWatcherCallback(
170-
vm,
171-
() => {
172-
pendingCallback = false;
173-
execCallback();
174-
},
175-
options.flush
176-
);
173+
vm.$nextTick(() => {
174+
flushWatcherCallback(
175+
vm,
176+
() => {
177+
pendingCallback = false;
178+
execCallback();
179+
},
180+
options.flush
181+
);
182+
});
177183
}
178184
};
179185

@@ -184,7 +190,11 @@ function createMuiltSourceWatcher<T>(
184190
} else {
185191
getter = source as () => T;
186192
}
187-
const watcherCtx = {} as WatcherContext<T>;
193+
const watcherCtx = {
194+
getter,
195+
value: initValue,
196+
oldValue: initValue,
197+
} as WatcherContext<T>;
188198
// must push watcherCtx before create watcherStopHandle
189199
watcherContext.push(watcherCtx);
190200

@@ -200,7 +210,8 @@ function createMuiltSourceWatcher<T>(
200210
immediate: !options.lazy,
201211
deep: options.deep,
202212
// @ts-ignore
203-
sync: options.flush === 'sync',
213+
// always set to true, so we can fully control the schedule
214+
sync: true,
204215
}
205216
);
206217
});
@@ -233,12 +244,15 @@ export function watch<T>(
233244
};
234245
let vm = getCurrentVM();
235246
if (!vm) {
236-
const Vue = getCurrentVue();
237-
const silent = Vue.config.silent;
238-
Vue.config.silent = true;
239-
vm = new Vue();
240-
Vue.config.silent = silent;
241-
opts.flush = 'auto';
247+
if (!fallbackVM) {
248+
const Vue = getCurrentVue();
249+
const silent = Vue.config.silent;
250+
Vue.config.silent = true;
251+
fallbackVM = new Vue();
252+
Vue.config.silent = silent;
253+
}
254+
vm = fallbackVM;
255+
opts.flush = 'sync';
242256
}
243257

244258
if (!hasWatchEnv(vm)) installWatchEnv(vm);

test/functions/watch.spec.js

Lines changed: 144 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ describe('Hooks watch', () => {
2323
a,
2424
};
2525
},
26-
});
27-
expect(spy).toHaveBeenCalled();
26+
template: `<div>{{a}}</div>`,
27+
}).$mount();
2828
expect(spy.mock.calls.length).toBe(1);
2929
expect(spy).toHaveBeenLastCalledWith(1, undefined);
3030
vm.a = 2;
3131
expect(spy.mock.calls.length).toBe(1);
32-
waitForWatcherFlush(() => {
32+
waitForUpdate(() => {
3333
expect(spy.mock.calls.length).toBe(2);
3434
expect(spy).toHaveBeenLastCalledWith(2, 1);
3535
}).then(done);
@@ -45,37 +45,96 @@ describe('Hooks watch', () => {
4545
a,
4646
};
4747
},
48-
});
49-
expect(spy).toHaveBeenCalled();
48+
template: `<div>{{a}}</div>`,
49+
}).$mount();
5050
expect(spy.mock.calls.length).toBe(1);
5151
expect(spy).toHaveBeenLastCalledWith(1, undefined);
5252
vm.a = 2;
5353
expect(spy.mock.calls.length).toBe(1);
54-
waitForWatcherFlush(() => {
54+
waitForUpdate(() => {
5555
expect(spy.mock.calls.length).toBe(2);
5656
expect(spy).toHaveBeenLastCalledWith(2, 1);
5757
}).then(done);
5858
});
5959

60-
it('basic usage(multiple sources)', done => {
60+
it('basic usage(multiple sources, lazy=false, flush=none-sync)', done => {
61+
const vm = new Vue({
62+
setup() {
63+
const a = value(1);
64+
const b = value(1);
65+
watch([a, b], spy, { lazy: false, flush: 'post' });
66+
67+
return {
68+
a,
69+
b,
70+
};
71+
},
72+
template: `<div>{{a}} {{b}}</div>`,
73+
}).$mount();
74+
expect(spy.mock.calls.length).toBe(1);
75+
expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]);
76+
vm.a = 2;
77+
expect(spy.mock.calls.length).toBe(1);
78+
waitForUpdate(() => {
79+
expect(spy.mock.calls.length).toBe(2);
80+
expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]);
81+
vm.a = 3;
82+
vm.b = 3;
83+
})
84+
.then(() => {
85+
expect(spy.mock.calls.length).toBe(3);
86+
expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]);
87+
})
88+
.then(done);
89+
});
90+
91+
it('basic usage(multiple sources, lazy=true, flush=none-sync)', done => {
92+
const vm = new Vue({
93+
setup() {
94+
const a = value(1);
95+
const b = value(1);
96+
watch([a, b], spy, { lazy: true, flush: 'post' });
97+
98+
return {
99+
a,
100+
b,
101+
};
102+
},
103+
template: `<div>{{a}} {{b}}</div>`,
104+
}).$mount();
105+
vm.a = 2;
106+
expect(spy).not.toHaveBeenCalled();
107+
waitForUpdate(() => {
108+
expect(spy.mock.calls.length).toBe(1);
109+
expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]);
110+
vm.a = 3;
111+
vm.b = 3;
112+
})
113+
.then(() => {
114+
expect(spy.mock.calls.length).toBe(2);
115+
expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]);
116+
})
117+
.then(done);
118+
});
119+
120+
it('basic usage(multiple sources, lazy=false, flush=sync)', done => {
61121
const vm = new Vue({
62122
setup() {
63123
const a = value(1);
64124
const b = value(1);
65-
watch([a, b], spy);
125+
watch([a, b], spy, { lazy: false, flush: 'sync' });
66126

67127
return {
68128
a,
69129
b,
70130
};
71131
},
72132
});
73-
expect(spy).toHaveBeenCalled();
74133
expect(spy.mock.calls.length).toBe(1);
75134
expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]);
76135
vm.a = 2;
77136
expect(spy.mock.calls.length).toBe(1);
78-
waitForWatcherFlush(() => {
137+
waitForUpdate(() => {
79138
expect(spy.mock.calls.length).toBe(2);
80139
expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]);
81140
vm.a = 3;
@@ -88,21 +147,79 @@ describe('Hooks watch', () => {
88147
.then(done);
89148
});
90149

150+
it('basic usage(multiple sources, lazy=true, flush=sync)', done => {
151+
const vm = new Vue({
152+
setup() {
153+
const a = value(1);
154+
const b = value(1);
155+
watch([a, b], spy, { lazy: true, flush: 'sync' });
156+
157+
return {
158+
a,
159+
b,
160+
};
161+
},
162+
});
163+
vm.a = 2;
164+
expect(spy).not.toHaveBeenCalled();
165+
waitForUpdate(() => {
166+
expect(spy.mock.calls.length).toBe(1);
167+
expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]);
168+
vm.a = 3;
169+
vm.b = 3;
170+
})
171+
.then(() => {
172+
expect(spy.mock.calls.length).toBe(2);
173+
expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]);
174+
})
175+
.then(done);
176+
});
177+
178+
it('out of setup', done => {
179+
const obj = state({ a: 1 });
180+
watch(() => obj.a, spy);
181+
expect(spy.mock.calls.length).toBe(1);
182+
expect(spy).toHaveBeenLastCalledWith(1, undefined);
183+
obj.a = 2;
184+
waitForUpdate(() => {
185+
expect(spy.mock.calls.length).toBe(2);
186+
expect(spy).toHaveBeenCalledWith(2, 1);
187+
}).then(done);
188+
});
189+
190+
it('out of setup(multiple sources)', done => {
191+
const obj1 = state({ a: 1 });
192+
const obj2 = state({ a: 2 });
193+
watch([() => obj1.a, () => obj2.a], spy);
194+
expect(spy.mock.calls.length).toBe(1);
195+
expect(spy).toHaveBeenLastCalledWith([1, 2], [undefined, undefined]);
196+
obj1.a = 2;
197+
obj2.a = 3;
198+
waitForUpdate(() => {
199+
expect(spy.mock.calls.length).toBe(2);
200+
expect(spy).toHaveBeenLastCalledWith([2, 3], [1, 2]);
201+
}).then(done);
202+
});
203+
91204
it('multiple cbs (after option merge)', done => {
92205
const spy1 = jest.fn();
93-
const obj = state({ a: 1 });
206+
const a = value(1);
94207
const Test = Vue.extend({
95208
setup() {
96-
watch(() => obj.a, spy1);
209+
watch(a, spy1);
97210
},
98211
});
99212
new Test({
100213
setup() {
101-
watch(() => obj.a, spy);
214+
watch(a, spy);
215+
return {
216+
a,
217+
};
102218
},
103-
});
104-
obj.a = 2;
105-
waitForWatcherFlush(() => {
219+
template: `<div>{{a}}</div>`,
220+
}).$mount();
221+
a.value = 2;
222+
waitForUpdate(() => {
106223
expect(spy1).toHaveBeenCalledWith(2, 1);
107224
expect(spy).toHaveBeenCalledWith(2, 1);
108225
}).then(done);
@@ -118,10 +235,11 @@ describe('Hooks watch', () => {
118235
a,
119236
};
120237
},
121-
});
238+
template: `<div>{{a}}</div>`,
239+
}).$mount();
122240
expect(spy).not.toHaveBeenCalled();
123241
vm.a = 2;
124-
waitForWatcherFlush(() => {
242+
waitForUpdate(() => {
125243
expect(spy).toHaveBeenCalledWith(2, 1);
126244
}).then(done);
127245
});
@@ -136,12 +254,13 @@ describe('Hooks watch', () => {
136254
a,
137255
};
138256
},
139-
});
257+
template: `<div>{{a}}</div>`,
258+
}).$mount();
140259
const oldA = vm.a;
141260
expect(spy).not.toHaveBeenCalled();
142261
vm.a.b = 2;
143262
expect(spy).not.toHaveBeenCalled();
144-
waitForWatcherFlush(() => {
263+
waitForUpdate(() => {
145264
expect(spy).toHaveBeenCalledWith(vm.a, vm.a);
146265
vm.a = { b: 3 };
147266
})
@@ -238,11 +357,14 @@ describe('Hooks watch', () => {
238357
数据: a,
239358
};
240359
},
241-
});
360+
render(h) {
361+
return h('div', this['数据']);
362+
},
363+
}).$mount();
242364
expect(spy).not.toHaveBeenCalled();
243365
vm['数据'] = 2;
244366
expect(spy).not.toHaveBeenCalled();
245-
waitForWatcherFlush(() => {
367+
waitForUpdate(() => {
246368
expect(spy).toHaveBeenCalledWith(2, 1);
247369
}).then(done);
248370
});

0 commit comments

Comments
 (0)
0