Skip to content

Necessary changes to observer for full Tracker compatibility #4652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions flow/global-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare interface GlobalAPI {
options: Object;
config: Config;
util: Object;
observer: Object;

extend: (options: Object) => Function;
set: <T>(target: Object | Array<T>, key: string | number, value: T) => T;
Expand Down
14 changes: 14 additions & 0 deletions src/core/global-api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you not using default import syntax?

L22: import Watcher from '../observer/watcher'
L20: import Dep, { pushTarget, popTarget } from '../observer/dep'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason. I can change it, if you prefer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find @znck syntax cleaner. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.


export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
Expand All @@ -40,6 +44,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
Expand Down
188 changes: 134 additions & 54 deletions src/core/observer/scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Function> = []

/**
* 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 = {}
Expand All @@ -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
Expand Down Expand Up @@ -139,10 +190,39 @@ 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)
}

/**
* 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
}
}
}
38 changes: 21 additions & 17 deletions src/core/observer/watcher.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow */

import { queueWatcher } from './scheduler'
import { queueWatcher, wrapWatcherGetter } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import {
Expand Down Expand Up @@ -82,6 +82,7 @@ export default class Watcher {
)
}
}
this.getter = wrapWatcherGetter(this.getter)
this.value = this.lazy
? undefined
: this.get()
Expand All @@ -92,25 +93,28 @@ export default class Watcher {
*/
get () {
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
try {
let value
const vm = this.vm
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
// "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
}

/**
Expand Down