From 92fd14a97ad26dafd64aaee7394f272c8350e97a Mon Sep 17 00:00:00 2001 From: Mitar Date: Thu, 5 Jan 2017 00:59:41 +0100 Subject: [PATCH 01/11] Necessary changes to observer for full Tracker compatibility. --- src/core/observer/scheduler.js | 196 ++++++++++++++++++++++++--------- src/core/observer/watcher.js | 23 ++-- src/core/util/index.js | 3 + 3 files changed, 160 insertions(+), 62 deletions(-) diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index 08182e1b86f..fec5c2d1384 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -14,13 +14,22 @@ let has: { [key: number]: ?true } = {} let circular: { [key: number]: number } = {} let waiting = false let flushing = false +let insideRun = false let index = 0 +const afterFlushCallbacks: Array = [] /** * Reset the scheduler's state. */ function resetSchedulerState () { - queue.length = 0 + // if we got to the end of the queue, we can just empty the queue + if (index === queue.length) { + queue.length = 0 + } + // else, we only remove watchers we ran + else { + queue.splice(0, index) + } has = {} if (process.env.NODE_ENV !== 'production') { circular = {} @@ -29,63 +38,115 @@ function resetSchedulerState () { } /** - * Flush both queues and run the watchers. + * Flush the queue and run the watchers. */ -function flushSchedulerQueue () { +function flushSchedulerQueue (maxUpdateCount?: number) { + if (flushing) { + throw new Error("Cannot flush while already flushing.") + } + + if (insideRun) { + throw new Error("Cannot flush while running a watcher.") + } + + maxUpdateCount = maxUpdateCount || config._maxUpdateCount + flushing = true - let watcher, id, vm - - // Sort queue before flush. - // This ensures that: - // 1. Components are updated from parent to child. (because parent is always - // created before the child) - // 2. A component's user watchers are run before its render watcher (because - // user watchers are created before the render watcher) - // 3. If a component is destroyed during a parent component's watcher run, - // its watchers can be skipped. - queue.sort((a, b) => a.id - b.id) - - // do not cache length because more watchers might be pushed - // as we run existing watchers - for (index = 0; index < queue.length; index++) { - watcher = queue[index] - id = watcher.id - has[id] = null - watcher.run() - // in dev build, check and stop circular updates. - if (process.env.NODE_ENV !== 'production' && has[id] != null) { - circular[id] = (circular[id] || 0) + 1 - if (circular[id] > config._maxUpdateCount) { - warn( - 'You may have an infinite update loop ' + ( - watcher.user - ? `in watcher with expression "${watcher.expression}"` - : `in a component render function.` - ), - watcher.vm - ) - break + let watcher, id, vm, hookIndex + + // a watcher's run can throw + try { + // Sort queue before flush. + // This ensures that: + // 1. Components are updated from parent to child. (because parent is always + // created before the child) + // 2. A component's user watchers are run before its render watcher (because + // user watchers are created before the render watcher) + // 3. If a component is destroyed during a parent component's watcher run, + // its watchers can be skipped. + queue.sort((a, b) => a.id - b.id) + + index = 0 + while (queue.length - index || afterFlushCallbacks.length) { + // do not cache length because more watchers might be pushed + // as we run existing watchers + for (; index < queue.length; index++) { + watcher = queue[index] + id = watcher.id + has[id] = null + watcher.run() + // in dev build, check and stop circular updates. + if (process.env.NODE_ENV !== 'production' && has[id] != null) { + circular[id] = (circular[id] || 0) + 1 + if (circular[id] > maxUpdateCount) { + warn( + 'You may have an infinite update loop ' + ( + watcher.user + ? `in watcher with expression "${watcher.expression}"` + : `in a component render function.` + ), + watcher.vm + ) + // to remove the whole current queue + index = queue.length + break + } + } + } + + if (afterFlushCallbacks.length) { + // call one afterFlush callback, which may queue more watchers + // TODO: Optimize to not modify array at every run. + let func = afterFlushCallbacks.shift() + try { + func() + } catch (e) { + /* istanbul ignore else */ + if (config.errorHandler) { + config.errorHandler.call(null, e) + } else { + process.env.NODE_ENV !== 'production' && warn( + `Error in an after flush callback.` + ) + throw e + } + } } } } + finally { + // a hook can throw as well + try { + // call updated hooks + hookIndex = index + while (hookIndex--) { + watcher = queue[hookIndex] + vm = watcher.vm + if (vm._watcher === watcher && vm._isMounted) { + callHook(vm, 'updated') + } + } - // call updated hooks - index = queue.length - while (index--) { - watcher = queue[index] - vm = watcher.vm - if (vm._watcher === watcher && vm._isMounted) { - callHook(vm, 'updated') + // devtool hook + /* istanbul ignore if */ + if (devtools && config.devtools) { + devtools.emit('flush') + } + } + finally { + resetSchedulerState() } } +} - // devtool hook - /* istanbul ignore if */ - if (devtools && config.devtools) { - devtools.emit('flush') +/** + * Queue the flush. + */ +function requireFlush () { + if (!waiting) { + waiting = true + nextTick(flushSchedulerQueue) } - - resetSchedulerState() } /** @@ -108,10 +169,39 @@ export function queueWatcher (watcher: Watcher) { } queue.splice(Math.max(i, index) + 1, 0, watcher) } - // queue the flush - if (!waiting) { - waiting = true - nextTick(flushSchedulerQueue) - } + requireFlush() } } + +/** + * Schedules a function to be called after the next flush, or later in the + * current flush if one is in progress, after all watchers have been rerun. + * The function will be run once and not on subsequent flushes unless + * `afterFlush` is called again. + */ +export function afterFlush (f: Function) { + afterFlushCallbacks.push(f) + requireFlush() +} + +/** + * Forces a synchronous flush. + */ +export function forceFlush (maxUpdateCount?: number) { + flushSchedulerQueue(maxUpdateCount) +} + +/** + * Used in watchers to wrap provided getters to set scheduler flags. + */ +export function wrapWatcherGetter (f: Function): Function { + return function (/* args */) { + const previousInsideRun = insideRun + insideRun = true + try { + return f.apply(this, arguments) + } finally { + insideRun = previousInsideRun + } + } +} \ No newline at end of file diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index c8aef73b0f1..6f6c5b9a43e 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -2,7 +2,7 @@ import config from '../config' import Dep, { pushTarget, popTarget } from './dep' -import { queueWatcher } from './scheduler' +import { queueWatcher, wrapWatcherGetter } from './scheduler' import { warn, remove, @@ -79,6 +79,7 @@ export default class Watcher { ) } } + this.getter = wrapWatcherGetter(this.getter); this.value = this.lazy ? undefined : this.get() @@ -89,15 +90,19 @@ export default class Watcher { */ get () { pushTarget(this) - const value = this.getter.call(this.vm, this.vm) - // "touch" every property so they are all tracked as - // dependencies for deep watching - if (this.deep) { - traverse(value) + try { + const value = this.getter.call(this.vm, this.vm) + // "touch" every property so they are all tracked as + // dependencies for deep watching + if (this.deep) { + traverse(value) + } + this.cleanupDeps() + return value + } + finally { + popTarget() } - popTarget() - this.cleanupDeps() - return value } /** diff --git a/src/core/util/index.js b/src/core/util/index.js index 2e06515cd65..e5d60eb7e5b 100644 --- a/src/core/util/index.js +++ b/src/core/util/index.js @@ -5,3 +5,6 @@ export * from './options' export * from './debug' export * from './props' export { defineReactive } from '../observer/index' +export { default as Dep, pushTarget, popTarget } from '../observer/dep' +export { afterFlush, forceFlush } from '../observer/scheduler' +export { default as Watcher } from '../observer/watcher' From b949953fdf0e5cb067a8d8465f62932c332644dd Mon Sep 17 00:00:00 2001 From: Mitar Date: Thu, 5 Jan 2017 09:03:00 +0100 Subject: [PATCH 02/11] Code style. --- src/core/observer/scheduler.js | 17 +++++++---------- src/core/observer/watcher.js | 5 ++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index fec5c2d1384..3cbbe386854 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -25,9 +25,8 @@ function resetSchedulerState () { // if we got to the end of the queue, we can just empty the queue if (index === queue.length) { queue.length = 0 - } // else, we only remove watchers we ran - else { + } else { queue.splice(0, index) } has = {} @@ -42,11 +41,11 @@ function resetSchedulerState () { */ function flushSchedulerQueue (maxUpdateCount?: number) { if (flushing) { - throw new Error("Cannot flush while already flushing.") + throw new Error('Cannot flush while already flushing.') } if (insideRun) { - throw new Error("Cannot flush while running a watcher.") + throw new Error('Cannot flush while running a watcher.') } maxUpdateCount = maxUpdateCount || config._maxUpdateCount @@ -97,7 +96,7 @@ function flushSchedulerQueue (maxUpdateCount?: number) { if (afterFlushCallbacks.length) { // call one afterFlush callback, which may queue more watchers // TODO: Optimize to not modify array at every run. - let func = afterFlushCallbacks.shift() + const func = afterFlushCallbacks.shift() try { func() } catch (e) { @@ -113,8 +112,7 @@ function flushSchedulerQueue (maxUpdateCount?: number) { } } } - } - finally { + } finally { // a hook can throw as well try { // call updated hooks @@ -132,8 +130,7 @@ function flushSchedulerQueue (maxUpdateCount?: number) { if (devtools && config.devtools) { devtools.emit('flush') } - } - finally { + } finally { resetSchedulerState() } } @@ -204,4 +201,4 @@ export function wrapWatcherGetter (f: Function): Function { insideRun = previousInsideRun } } -} \ No newline at end of file +} diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index 6f6c5b9a43e..338759ff41b 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -79,7 +79,7 @@ export default class Watcher { ) } } - this.getter = wrapWatcherGetter(this.getter); + this.getter = wrapWatcherGetter(this.getter) this.value = this.lazy ? undefined : this.get() @@ -99,8 +99,7 @@ export default class Watcher { } this.cleanupDeps() return value - } - finally { + } finally { popTarget() } } From 4f637c64c3085ee7ed3709ababdf9193ddfa1171 Mon Sep 17 00:00:00 2001 From: Mitar Date: Tue, 28 Feb 2017 14:49:49 -0800 Subject: [PATCH 03/11] Do not expose observer methods through util. --- src/core/global-api/index.js | 14 ++++++++++++++ src/core/util/index.js | 3 --- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/core/global-api/index.js b/src/core/global-api/index.js index fd53b1d78b0..6b66700e66c 100644 --- a/src/core/global-api/index.js +++ b/src/core/global-api/index.js @@ -16,6 +16,10 @@ import { defineReactive } from '../util/index' +import { default as Dep, pushTarget, popTarget } from '../observer/dep' +import { afterFlush, forceFlush } from '../observer/scheduler' +import { default as Watcher } from '../observer/watcher' + export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} @@ -39,6 +43,16 @@ export function initGlobalAPI (Vue: GlobalAPI) { defineReactive } + // exposed observer methods. + Vue.observer = { + Dep, + pushTarget, + popTarget, + afterFlush, + forceFlush, + Watcher + } + Vue.set = set Vue.delete = del Vue.nextTick = nextTick diff --git a/src/core/util/index.js b/src/core/util/index.js index 559f0146e33..8ccff396bbc 100644 --- a/src/core/util/index.js +++ b/src/core/util/index.js @@ -6,6 +6,3 @@ export * from './debug' export * from './props' export * from './error' export { defineReactive } from '../observer/index' -export { default as Dep, pushTarget, popTarget } from '../observer/dep' -export { afterFlush, forceFlush } from '../observer/scheduler' -export { default as Watcher } from '../observer/watcher' From 3808d5fc0d1034b7e22bd2a79a6ae196170b23c0 Mon Sep 17 00:00:00 2001 From: Mitar Date: Tue, 28 Feb 2017 20:03:02 -0800 Subject: [PATCH 04/11] Fixing tests. --- src/core/observer/scheduler.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index 3cbbe386854..94d7b5f656a 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -6,7 +6,8 @@ import { callHook } from '../instance/lifecycle' import { warn, nextTick, - devtools + devtools, + handleError } from '../util/index' const queue: Array = [] @@ -100,15 +101,7 @@ function flushSchedulerQueue (maxUpdateCount?: number) { try { func() } catch (e) { - /* istanbul ignore else */ - if (config.errorHandler) { - config.errorHandler.call(null, e) - } else { - process.env.NODE_ENV !== 'production' && warn( - `Error in an after flush callback.` - ) - throw e - } + handleError(e, null, `Error in an after flush callback.`) } } } From 6424581cb350f254d006aa3990d3731c5b409a38 Mon Sep 17 00:00:00 2001 From: Mitar Date: Tue, 28 Feb 2017 20:05:59 -0800 Subject: [PATCH 05/11] Missed file to commit. --- flow/global-api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/flow/global-api.js b/flow/global-api.js index c467f147545..2aa567c530d 100644 --- a/flow/global-api.js +++ b/flow/global-api.js @@ -3,6 +3,7 @@ declare interface GlobalAPI { options: Object; config: Config; util: Object; + observer: Object; extend: (options: Object) => Function; set: (obj: Object, key: string, value: any) => void; From dd6bd19a7897c4277170d76965e380c7ed2dba66 Mon Sep 17 00:00:00 2001 From: Mitar Date: Wed, 28 Jun 2017 05:24:24 -0700 Subject: [PATCH 06/11] Fixed bad merge. --- src/core/observer/scheduler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index 4e992eee84c..3706a7904c0 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -33,8 +33,8 @@ function resetSchedulerState () { // else, we only remove watchers we ran } else { queue.splice(0, index) - activatedChildren.splice(0, index) index = 0 + activatedChildren.length = 0 } has = {} if (process.env.NODE_ENV !== 'production') { @@ -120,7 +120,7 @@ function flushSchedulerQueue (maxUpdateCount?: number) { resetSchedulerState() // call component updated and activated hooks - callActivatedHooks(activatedQueue, endIndex) + callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue, endIndex) // devtool hook @@ -163,8 +163,8 @@ export function queueActivatedComponent (vm: Component) { activatedChildren.push(vm) } -function callActivatedHooks (queue, endIndex) { - for (let i = 0; i < endIndex; i++) { +function callActivatedHooks (queue) { + for (let i = 0; i < queue.length; i++) { queue[i]._inactive = true activateChildComponent(queue[i], true /* true */) } From e1ecf7ad8c15178620ac0cd428785cf53c2245b6 Mon Sep 17 00:00:00 2001 From: Mitar Date: Wed, 28 Jun 2017 05:44:49 -0700 Subject: [PATCH 07/11] Simpler import syntax. --- src/core/global-api/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/global-api/index.js b/src/core/global-api/index.js index acd58459559..7ee50d983b2 100644 --- a/src/core/global-api/index.js +++ b/src/core/global-api/index.js @@ -17,9 +17,9 @@ import { defineReactive } from '../util/index' -import { default as Dep, pushTarget, popTarget } from '../observer/dep' +import Dep, { pushTarget, popTarget } from '../observer/dep' import { afterFlush, forceFlush } from '../observer/scheduler' -import { default as Watcher } from '../observer/watcher' +import Watcher from '../observer/watcher' export function initGlobalAPI (Vue: GlobalAPI) { // config From 09f8aa6442587cad4181402ea56cc95e70754266 Mon Sep 17 00:00:00 2001 From: Mitar Date: Sat, 8 Jul 2017 02:11:17 -0700 Subject: [PATCH 08/11] Added isFlushing. --- src/core/global-api/index.js | 3 ++- src/core/observer/scheduler.js | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/global-api/index.js b/src/core/global-api/index.js index 7ee50d983b2..07bf39de957 100644 --- a/src/core/global-api/index.js +++ b/src/core/global-api/index.js @@ -18,7 +18,7 @@ import { } from '../util/index' import Dep, { pushTarget, popTarget } from '../observer/dep' -import { afterFlush, forceFlush } from '../observer/scheduler' +import { afterFlush, forceFlush, isFlushing } from '../observer/scheduler' import Watcher from '../observer/watcher' export function initGlobalAPI (Vue: GlobalAPI) { @@ -51,6 +51,7 @@ export function initGlobalAPI (Vue: GlobalAPI) { popTarget, afterFlush, forceFlush, + isFlushing, Watcher } diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index 3706a7904c0..fd89d922d3d 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -212,6 +212,13 @@ export function forceFlush (maxUpdateCount?: number) { flushSchedulerQueue(maxUpdateCount) } +/** + * Are we inside a flush? + */ +export function isFlushing () { + return flushing; +} + /** * Used in watchers to wrap provided getters to set scheduler flags. */ From f054458a35ee3ec8b3175914abc82d6a2652d07c Mon Sep 17 00:00:00 2001 From: Mitar Date: Sat, 8 Jul 2017 02:20:01 -0700 Subject: [PATCH 09/11] Style. --- src/core/observer/scheduler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/observer/scheduler.js b/src/core/observer/scheduler.js index fd89d922d3d..d3e4826e6f0 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -216,7 +216,7 @@ export function forceFlush (maxUpdateCount?: number) { * Are we inside a flush? */ export function isFlushing () { - return flushing; + return flushing } /** From 7503c2e4ba9b4e94bcf9cc459ef7fbb5d6e59eaf Mon Sep 17 00:00:00 2001 From: Mitar Date: Tue, 11 Jul 2017 20:11:40 -0700 Subject: [PATCH 10/11] Fixed an edge case in Tracker.nonreactive. --- src/core/observer/dep.js | 2 +- src/core/observer/watcher.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/observer/dep.js b/src/core/observer/dep.js index 5d55e9cd421..b48df95d7ca 100644 --- a/src/core/observer/dep.js +++ b/src/core/observer/dep.js @@ -49,7 +49,7 @@ Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { - if (Dep.target) targetStack.push(Dep.target) + targetStack.push(Dep.target) Dep.target = _target } diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index f7894c6121a..09bce184112 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -91,7 +91,7 @@ export default class Watcher { /** * Evaluate the getter, and re-collect dependencies. */ - get () { + get (dontCleanupDeps: ?boolean) { pushTarget(this) let value const vm = this.vm @@ -110,7 +110,9 @@ export default class Watcher { traverse(value) } popTarget() - this.cleanupDeps() + if (!dontCleanupDeps) { + this.cleanupDeps() + } } return value } From 99483b72123f6d966782bad35f2a090813d64a16 Mon Sep 17 00:00:00 2001 From: Mitar Date: Sun, 5 Nov 2017 08:56:51 -0800 Subject: [PATCH 11/11] Allow reference to watcher to be available during create. --- src/core/instance/lifecycle.js | 3 ++- src/core/observer/watcher.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/instance/lifecycle.js b/src/core/instance/lifecycle.js index db7bf286f11..692606c5054 100644 --- a/src/core/instance/lifecycle.js +++ b/src/core/instance/lifecycle.js @@ -193,7 +193,8 @@ export function mountComponent ( } } - vm._watcher = new Watcher(vm, updateComponent, noop) + vm._watcher = new Watcher(vm, updateComponent, noop, { delayed: true }) + vm._watcher.get() hydrating = false // manually mounted instance, call mounted on self diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index 09bce184112..cff914e5cd5 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -83,7 +83,7 @@ export default class Watcher { } } this.getter = wrapWatcherGetter(this.getter) - this.value = this.lazy + this.value = this.lazy || (options && options.delayed) ? undefined : this.get() }