refactor: watch APIs default to trigger pre-flush
BREAKING CHANGE: watch APIs now default to use `flush: 'pre'` instead of
`flush: 'post'`.
  - This change affects `watch`, `watchEffect`, the `watch` component
    option, and `this.$watch`.
  - As pointed out by @skirtles-code in
    [this comment](https://github.com/vuejs/vue-next/issues/1706#issuecomment-666258948),
    Vue 2's watch behavior is pre-flush, and the ecosystem has many uses
    of watch that assumes the pre-flush behavior. Defaulting to post-flush
    can result in unnecessary re-renders without the users being aware of
    it.
  - With this change, watchers need to specify `{ flush: 'post' }` via
    options to trigger callback after Vue render updates. Note that
    specifying `{ flush: 'post' }` will also defer `watchEffect`'s
    initial run to wait for the component's initial render.
			
			
This commit is contained in:
		
							parent
							
								
									58c31e3699
								
							
						
					
					
						commit
						49bb44756f
					
				| @ -280,38 +280,7 @@ describe('api: watch', () => { | |||||||
|     expect(cleanup).toHaveBeenCalledTimes(2) |     expect(cleanup).toHaveBeenCalledTimes(2) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   it('flush timing: post (default)', async () => { |   it('flush timing: pre (default)', async () => { | ||||||
|     const count = ref(0) |  | ||||||
|     let callCount = 0 |  | ||||||
|     let result |  | ||||||
|     const assertion = jest.fn(count => { |  | ||||||
|       callCount++ |  | ||||||
|       // on mount, the watcher callback should be called before DOM render
 |  | ||||||
|       // on update, should be called after the count is updated
 |  | ||||||
|       const expectedDOM = callCount === 1 ? `` : `${count}` |  | ||||||
|       result = serializeInner(root) === expectedDOM |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const Comp = { |  | ||||||
|       setup() { |  | ||||||
|         watchEffect(() => { |  | ||||||
|           assertion(count.value) |  | ||||||
|         }) |  | ||||||
|         return () => count.value |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     const root = nodeOps.createElement('div') |  | ||||||
|     render(h(Comp), root) |  | ||||||
|     expect(assertion).toHaveBeenCalledTimes(1) |  | ||||||
|     expect(result).toBe(true) |  | ||||||
| 
 |  | ||||||
|     count.value++ |  | ||||||
|     await nextTick() |  | ||||||
|     expect(assertion).toHaveBeenCalledTimes(2) |  | ||||||
|     expect(result).toBe(true) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   it('flush timing: pre', async () => { |  | ||||||
|     const count = ref(0) |     const count = ref(0) | ||||||
|     const count2 = ref(0) |     const count2 = ref(0) | ||||||
| 
 | 
 | ||||||
| @ -332,14 +301,9 @@ describe('api: watch', () => { | |||||||
| 
 | 
 | ||||||
|     const Comp = { |     const Comp = { | ||||||
|       setup() { |       setup() { | ||||||
|         watchEffect( |         watchEffect(() => { | ||||||
|           () => { |  | ||||||
|           assertion(count.value, count2.value) |           assertion(count.value, count2.value) | ||||||
|           }, |         }) | ||||||
|           { |  | ||||||
|             flush: 'pre' |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|         return () => count.value |         return () => count.value | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -358,6 +322,35 @@ describe('api: watch', () => { | |||||||
|     expect(result2).toBe(true) |     expect(result2).toBe(true) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |   it('flush timing: post', async () => { | ||||||
|  |     const count = ref(0) | ||||||
|  |     let result | ||||||
|  |     const assertion = jest.fn(count => { | ||||||
|  |       result = serializeInner(root) === `${count}` | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const Comp = { | ||||||
|  |       setup() { | ||||||
|  |         watchEffect( | ||||||
|  |           () => { | ||||||
|  |             assertion(count.value) | ||||||
|  |           }, | ||||||
|  |           { flush: 'post' } | ||||||
|  |         ) | ||||||
|  |         return () => count.value | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const root = nodeOps.createElement('div') | ||||||
|  |     render(h(Comp), root) | ||||||
|  |     expect(assertion).toHaveBeenCalledTimes(1) | ||||||
|  |     expect(result).toBe(true) | ||||||
|  | 
 | ||||||
|  |     count.value++ | ||||||
|  |     await nextTick() | ||||||
|  |     expect(assertion).toHaveBeenCalledTimes(2) | ||||||
|  |     expect(result).toBe(true) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|   it('flush timing: sync', async () => { |   it('flush timing: sync', async () => { | ||||||
|     const count = ref(0) |     const count = ref(0) | ||||||
|     const count2 = ref(0) |     const count2 = ref(0) | ||||||
| @ -410,7 +403,7 @@ describe('api: watch', () => { | |||||||
|     const cb = jest.fn() |     const cb = jest.fn() | ||||||
|     const Comp = { |     const Comp = { | ||||||
|       setup() { |       setup() { | ||||||
|         watch(toggle, cb) |         watch(toggle, cb, { flush: 'post' }) | ||||||
|       }, |       }, | ||||||
|       render() {} |       render() {} | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -154,7 +154,7 @@ describe('Suspense', () => { | |||||||
|     expect(onResolve).toHaveBeenCalled() |     expect(onResolve).toHaveBeenCalled() | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   test('buffer mounted/updated hooks & watch callbacks', async () => { |   test('buffer mounted/updated hooks & post flush watch callbacks', async () => { | ||||||
|     const deps: Promise<any>[] = [] |     const deps: Promise<any>[] = [] | ||||||
|     const calls: string[] = [] |     const calls: string[] = [] | ||||||
|     const toggle = ref(true) |     const toggle = ref(true) | ||||||
| @ -165,14 +165,21 @@ describe('Suspense', () => { | |||||||
|         // extra tick needed for Node 12+
 |         // extra tick needed for Node 12+
 | ||||||
|         deps.push(p.then(() => Promise.resolve())) |         deps.push(p.then(() => Promise.resolve())) | ||||||
| 
 | 
 | ||||||
|         watchEffect(() => { |         watchEffect( | ||||||
|           calls.push('immediate effect') |           () => { | ||||||
|         }) |             calls.push('watch effect') | ||||||
|  |           }, | ||||||
|  |           { flush: 'post' } | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         const count = ref(0) |         const count = ref(0) | ||||||
|         watch(count, () => { |         watch( | ||||||
|  |           count, | ||||||
|  |           () => { | ||||||
|             calls.push('watch callback') |             calls.push('watch callback') | ||||||
|         }) |           }, | ||||||
|  |           { flush: 'post' } | ||||||
|  |         ) | ||||||
|         count.value++ // trigger the watcher now
 |         count.value++ // trigger the watcher now
 | ||||||
| 
 | 
 | ||||||
|         onMounted(() => { |         onMounted(() => { | ||||||
| @ -201,12 +208,12 @@ describe('Suspense', () => { | |||||||
|     const root = nodeOps.createElement('div') |     const root = nodeOps.createElement('div') | ||||||
|     render(h(Comp), root) |     render(h(Comp), root) | ||||||
|     expect(serializeInner(root)).toBe(`<div>fallback</div>`) |     expect(serializeInner(root)).toBe(`<div>fallback</div>`) | ||||||
|     expect(calls).toEqual([`immediate effect`]) |     expect(calls).toEqual([]) | ||||||
| 
 | 
 | ||||||
|     await Promise.all(deps) |     await Promise.all(deps) | ||||||
|     await nextTick() |     await nextTick() | ||||||
|     expect(serializeInner(root)).toBe(`<div>async</div>`) |     expect(serializeInner(root)).toBe(`<div>async</div>`) | ||||||
|     expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`]) |     expect(calls).toEqual([`watch effect`, `watch callback`, `mounted`]) | ||||||
| 
 | 
 | ||||||
|     // effects inside an already resolved suspense should happen at normal timing
 |     // effects inside an already resolved suspense should happen at normal timing
 | ||||||
|     toggle.value = false |     toggle.value = false | ||||||
| @ -214,7 +221,7 @@ describe('Suspense', () => { | |||||||
|     await nextTick() |     await nextTick() | ||||||
|     expect(serializeInner(root)).toBe(`<!---->`) |     expect(serializeInner(root)).toBe(`<!---->`) | ||||||
|     expect(calls).toEqual([ |     expect(calls).toEqual([ | ||||||
|       `immediate effect`, |       `watch effect`, | ||||||
|       `watch callback`, |       `watch callback`, | ||||||
|       `mounted`, |       `mounted`, | ||||||
|       'unmounted' |       'unmounted' | ||||||
| @ -319,14 +326,21 @@ describe('Suspense', () => { | |||||||
|         const p = new Promise(r => setTimeout(r, 1)) |         const p = new Promise(r => setTimeout(r, 1)) | ||||||
|         deps.push(p) |         deps.push(p) | ||||||
| 
 | 
 | ||||||
|         watchEffect(() => { |         watchEffect( | ||||||
|           calls.push('immediate effect') |           () => { | ||||||
|         }) |             calls.push('watch effect') | ||||||
|  |           }, | ||||||
|  |           { flush: 'post' } | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         const count = ref(0) |         const count = ref(0) | ||||||
|         watch(count, () => { |         watch( | ||||||
|  |           count, | ||||||
|  |           () => { | ||||||
|             calls.push('watch callback') |             calls.push('watch callback') | ||||||
|         }) |           }, | ||||||
|  |           { flush: 'post' } | ||||||
|  |         ) | ||||||
|         count.value++ // trigger the watcher now
 |         count.value++ // trigger the watcher now
 | ||||||
| 
 | 
 | ||||||
|         onMounted(() => { |         onMounted(() => { | ||||||
| @ -355,7 +369,7 @@ describe('Suspense', () => { | |||||||
|     const root = nodeOps.createElement('div') |     const root = nodeOps.createElement('div') | ||||||
|     render(h(Comp), root) |     render(h(Comp), root) | ||||||
|     expect(serializeInner(root)).toBe(`<div>fallback</div>`) |     expect(serializeInner(root)).toBe(`<div>fallback</div>`) | ||||||
|     expect(calls).toEqual(['immediate effect']) |     expect(calls).toEqual([]) | ||||||
| 
 | 
 | ||||||
|     // remove the async dep before it's resolved
 |     // remove the async dep before it's resolved
 | ||||||
|     toggle.value = false |     toggle.value = false | ||||||
| @ -366,8 +380,8 @@ describe('Suspense', () => { | |||||||
|     await Promise.all(deps) |     await Promise.all(deps) | ||||||
|     await nextTick() |     await nextTick() | ||||||
|     expect(serializeInner(root)).toBe(`<!---->`) |     expect(serializeInner(root)).toBe(`<!---->`) | ||||||
|     // should discard effects (except for immediate ones)
 |     // should discard effects (except for unmount)
 | ||||||
|     expect(calls).toEqual(['immediate effect', 'unmounted']) |     expect(calls).toEqual(['unmounted']) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   test('unmount suspense after resolve', async () => { |   test('unmount suspense after resolve', async () => { | ||||||
|  | |||||||
| @ -268,9 +268,10 @@ function doWatch( | |||||||
|   let scheduler: (job: () => any) => void |   let scheduler: (job: () => any) => void | ||||||
|   if (flush === 'sync') { |   if (flush === 'sync') { | ||||||
|     scheduler = job |     scheduler = job | ||||||
|   } else if (flush === 'pre') { |   } else if (flush === 'post') { | ||||||
|     // ensure it's queued before component updates (which have positive ids)
 |     scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) | ||||||
|     job.id = -1 |   } else { | ||||||
|  |     // default: 'pre'
 | ||||||
|     scheduler = () => { |     scheduler = () => { | ||||||
|       if (!instance || instance.isMounted) { |       if (!instance || instance.isMounted) { | ||||||
|         queuePreFlushCb(job) |         queuePreFlushCb(job) | ||||||
| @ -280,8 +281,6 @@ function doWatch( | |||||||
|         job() |         job() | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } else { |  | ||||||
|     scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const runner = effect(getter, { |   const runner = effect(getter, { | ||||||
| @ -300,6 +299,8 @@ function doWatch( | |||||||
|     } else { |     } else { | ||||||
|       oldValue = runner() |       oldValue = runner() | ||||||
|     } |     } | ||||||
|  |   } else if (flush === 'post') { | ||||||
|  |     queuePostRenderEffect(runner, instance && instance.suspense) | ||||||
|   } else { |   } else { | ||||||
|     runner() |     runner() | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -171,15 +171,18 @@ const KeepAliveImpl = { | |||||||
|       keys.delete(key) |       keys.delete(key) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // prune cache on include/exclude prop change
 | ||||||
|     watch( |     watch( | ||||||
|       () => [props.include, props.exclude], |       () => [props.include, props.exclude], | ||||||
|       ([include, exclude]) => { |       ([include, exclude]) => { | ||||||
|         include && pruneCache(name => matches(include, name)) |         include && pruneCache(name => matches(include, name)) | ||||||
|         exclude && pruneCache(name => !matches(exclude, name)) |         exclude && pruneCache(name => !matches(exclude, name)) | ||||||
|       } |       }, | ||||||
|  |       // prune post-render after `current` has been updated
 | ||||||
|  |       { flush: 'post' } | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     // cache sub tree in beforeMount/Update (i.e. right after the render)
 |     // cache sub tree after render
 | ||||||
|     let pendingCacheKey: CacheKey | null = null |     let pendingCacheKey: CacheKey | null = null | ||||||
|     const cacheSubtree = () => { |     const cacheSubtree = () => { | ||||||
|       // fix #1621, the pendingCacheKey could be 0
 |       // fix #1621, the pendingCacheKey could be 0
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user