Skip to content

Commit

Permalink
fix: further adjust nextTick strategy
Browse files Browse the repository at this point in the history
fix #6813
  • Loading branch information
yyx990803 committed Oct 13, 2017
1 parent 6658b81 commit 4e0c485
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 101 deletions.
85 changes: 0 additions & 85 deletions src/core/util/env.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
/* @flow */
/* globals MessageChannel */

import { handleError } from './error'

// can we use __proto__?
export const hasProto = '__proto__' in {}
Expand Down Expand Up @@ -62,88 +59,6 @@ export const hasSymbol =
typeof Symbol !== 'undefined' && isNative(Symbol) &&
typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)

/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc

function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and the only polyfill
// that consistently queues the callback after all DOM events triggered in the
// same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}

return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()

let _Set
/* istanbul ignore if */ // $flow-disable-line
if (typeof Set !== 'undefined' && isNative(Set)) {
Expand Down
1 change: 1 addition & 0 deletions src/core/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from './options'
export * from './debug'
export * from './props'
export * from './error'
export * from './next-tick'
export { defineReactive } from '../observer/index'
116 changes: 116 additions & 0 deletions src/core/util/next-tick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
let pending = false

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// Here we have async deferring wrappers using both micro and macro tasks.
// In < 2.4 we used micro tasks everywhere, but there are some scenarios where
// micro tasks have too high a priority and fires in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using macro tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use micro task by default, but expose a way to force macro task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) Task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

// Determine MicroTask defer implementation.
// $flow-disable-line, istanbul ignore next
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}

/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a Task instead of a MicroTask.
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}

export function nextTick (cb?: Function, ctx?: Object): ?Promise {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
32 changes: 18 additions & 14 deletions src/platforms/web/runtime/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { isDef, isUndef } from 'shared/util'
import { updateListeners } from 'core/vdom/helpers/index'
import { isIE, supportsPassive } from 'core/util/env'
import { withMacroTask, isIE, supportsPassive } from 'core/util/index'
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'

// normalize v-model event tokens that can only be determined at runtime.
Expand All @@ -28,25 +28,25 @@ function normalizeEvents (on) {

let target: HTMLElement

function createOnceHandler (handler, event, capture) {
const _target = target // save current target element in closure
return function onceHandler () {
const res = handler.apply(null, arguments)
if (res !== null) {
remove(event, onceHandler, capture, _target)
}
}
}

function add (
event: string,
handler: Function,
once: boolean,
capture: boolean,
passive: boolean
) {
if (once) {
const oldHandler = handler
const _target = target // save current target element in closure
handler = function (ev) {
const res = arguments.length === 1
? oldHandler(ev)
: oldHandler.apply(null, arguments)
if (res !== null) {
remove(event, handler, capture, _target)
}
}
}
handler = withMacroTask(handler)
if (once) handler = createOnceHandler(handler, event, capture)
target.addEventListener(
event,
handler,
Expand All @@ -62,7 +62,11 @@ function remove (
capture: boolean,
_target?: HTMLElement
) {
(_target || target).removeEventListener(event, handler, capture)
(_target || target).removeEventListener(
event,
handler._withTask || handler,
capture
)
}

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/specs/async-edge-cases.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

<!-- #6566 click event bubbling -->
<div id="case-2">
<div v-if="expand">
<div class="panel" v-if="expand">
<button @click="expand = false, countA++">Expand is True</button>
</div>
<div class="header" v-if="!expand" @click="expand = true, countB++">
Expand Down
2 changes: 1 addition & 1 deletion test/unit/modules/util/next-tick.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nextTick } from 'core/util/env'
import { nextTick } from 'core/util/next-tick'

describe('nextTick', () => {
it('accepts a callback', done => {
Expand Down

0 comments on commit 4e0c485

Please sign in to comment.