fix: further adjust nextTick strategy

fix #6813
This commit is contained in:
Evan You 2017-10-13 15:37:52 -04:00
parent 6658b81a3b
commit 4e0c48511d
6 changed files with 137 additions and 101 deletions

View File

@ -1,7 +1,4 @@
/* @flow */
/* globals MessageChannel */
import { handleError } from './error'
// can we use __proto__?
export const hasProto = '__proto__' in {}
@ -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)) {

View File

@ -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
src/core/util/next-tick.js Normal file
View File

@ -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
})
}
}

View File

@ -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.
@ -28,6 +28,16 @@ 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,
@ -35,18 +45,8 @@ function add (
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,
@ -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) {

View File

@ -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++">

View File

@ -1,4 +1,4 @@
import { nextTick } from 'core/util/env'
import { nextTick } from 'core/util/next-tick'
describe('nextTick', () => {
it('accepts a callback', done => {