diff --git a/flow/global-api.js b/flow/global-api.js index 0b2efb23ac3..1369e1835f6 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: (target: Object | Array, key: string | number, value: T) => T; diff --git a/src/core/global-api/index.js b/src/core/global-api/index.js index 03ffa0fbf31..07bf39de957 100644 --- a/src/core/global-api/index.js +++ b/src/core/global-api/index.js @@ -17,6 +17,10 @@ import { defineReactive } from '../util/index' +import Dep, { pushTarget, popTarget } from '../observer/dep' +import { afterFlush, forceFlush, isFlushing } from '../observer/scheduler' +import Watcher from '../observer/watcher' + export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} @@ -40,6 +44,17 @@ export function initGlobalAPI (Vue: GlobalAPI) { defineReactive } + // exposed observer methods. + Vue.observer = { + Dep, + pushTarget, + popTarget, + afterFlush, + forceFlush, + isFlushing, + Watcher + } + Vue.set = set Vue.delete = del Vue.nextTick = nextTick 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/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/scheduler.js b/src/core/observer/scheduler.js index fce86e5f40b..d3e4826e6f0 100644 --- a/src/core/observer/scheduler.js +++ b/src/core/observer/scheduler.js @@ -7,7 +7,8 @@ import { callHook, activateChildComponent } from '../instance/lifecycle' import { warn, nextTick, - devtools + devtools, + handleError } from '../util/index' export const MAX_UPDATE_COUNT = 100 @@ -18,13 +19,23 @@ 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 () { - index = queue.length = activatedChildren.length = 0 + // if we got to the end of the queue, we can just empty the queue + if (index === queue.length) { + index = queue.length = activatedChildren.length = 0 + // else, we only remove watchers we ran + } else { + queue.splice(0, index) + index = 0 + activatedChildren.length = 0 + } has = {} if (process.env.NODE_ENV !== 'production') { circular = {} @@ -33,65 +44,105 @@ 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 || MAX_UPDATE_COUNT + flushing = true let watcher, id - // 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] > MAX_UPDATE_COUNT) { - warn( - 'You may have an infinite update loop ' + ( - watcher.user - ? `in watcher with expression "${watcher.expression}"` - : `in a component render function.` - ), - watcher.vm - ) - break + // 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. + const func = afterFlushCallbacks.shift() + try { + func() + } catch (e) { + handleError(e, null, `Error in an after flush callback.`) + } } } + } finally { + // keep copies of post queues before resetting state + const activatedQueue = activatedChildren.slice() + const updatedQueue = queue.slice() + const endIndex = index + + resetSchedulerState() + + // call component updated and activated hooks + callActivatedHooks(activatedQueue) + callUpdatedHooks(updatedQueue, endIndex) + + // devtool hook + /* istanbul ignore if */ + if (devtools && config.devtools) { + devtools.emit('flush') + } } +} - // keep copies of post queues before resetting state - const activatedQueue = activatedChildren.slice() - const updatedQueue = queue.slice() - - resetSchedulerState() - - // call component updated and activated hooks - callActivatedHooks(activatedQueue) - callUpdatedHooks(updatedQueue) - - // devtool hook - /* istanbul ignore if */ - if (devtools && config.devtools) { - devtools.emit('flush') +/** + * Queue the flush. + */ +function requireFlush () { + if (!waiting) { + waiting = true + nextTick(flushSchedulerQueue) } } -function callUpdatedHooks (queue) { - let i = queue.length +function callUpdatedHooks (queue, endIndex) { + let i = endIndex while (i--) { const watcher = queue[i] const vm = watcher.vm @@ -139,10 +190,46 @@ export function queueWatcher (watcher: Watcher) { } queue.splice(i + 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) +} + +/** + * Are we inside a flush? + */ +export function isFlushing () { + return flushing +} + +/** + * 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 } } } diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index 40253149c34..cff914e5cd5 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -1,6 +1,6 @@ /* @flow */ -import { queueWatcher } from './scheduler' +import { queueWatcher, wrapWatcherGetter } from './scheduler' import Dep, { pushTarget, popTarget } from './dep' import { @@ -82,7 +82,8 @@ export default class Watcher { ) } } - this.value = this.lazy + this.getter = wrapWatcherGetter(this.getter) + this.value = this.lazy || (options && options.delayed) ? undefined : this.get() } @@ -90,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 @@ -109,7 +110,9 @@ export default class Watcher { traverse(value) } popTarget() - this.cleanupDeps() + if (!dontCleanupDeps) { + this.cleanupDeps() + } } return value }