1.定义
- 在DOM更新完毕之后执行一个延迟的回调,在修改数据后可以立即获取最新dom内容。
- 是vue的全局api
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () { // DOM 更新了
})
2.在什么场景使用
在更新data后,想获得最新的dom,则需要使用nextTick方法在里面才能访问最新dom。
3.如何使用
nextTick (cb?: Function, ctx?: Object) 支持两个参数 cb回调方法,ctx上下文。
//调用方式1
$nextTick(()=> {
//....
})
//调用方式2
Vue.nextTick(()=> {
//....
})
4.异步更新队列的实现原理
4.1更新流程图
4.2更新原理
- vue更新dom是异步进行的。只要监听到数据变化,Vue就会开启一个队列,并且缓冲同一事件循环中的所有数据变化。如果同一个watcher被调用多次,最终只会被推入队列一次。
- 这种缓冲时,去除重复数据的操作,有效的减少不必要的计算与dom的实际操作。
- 并且nextTick是插入到队列的最后面执行。所以能拿到最新的dom数据。
4.3事件循环(Event Loop)
- 在js的运行环境中,通常伴随着很多事件的发生,比如用户点击、页面渲染、脚本执行、网络请求等等。为了协调这些事件的处理,浏览器使用事件循环机制。
- 简要来说,事件循环会维护一个或多个任务队列(task queues),事件作为任务源往队列中加入任务。
- 有一个持续执行的线程来处理这些任务,每执行完一个就从队列中移除它,这就是一次事件循环。
4.4macrotask宏任务
macrotask是宏任务
- 每一次事件循环都会在宏队列里,执行一个宏任务。
4.5microtask微任务
microtask是微任务
- 每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后再开始下一次事件循环。
- 在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。
- macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。
- microtask的这一特性,是做队列控制的最佳选择。
- vue进行DOM更新内部也是调用nextTick来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。
4.6实现microtask方法
- Promise
- MutationObserver (2.5版本后被移除,HTML5新增的特性,因为在iOS有bug)
- Object.observe(废弃)
- nodejs中的 process.nextTick.
4.7vue的降级策略
队列控制的最佳选择是microtask,而microtask的最佳选择是Promise。如果当前环境不支持Promise,vue就不得不降级为macrotask来做队列控制了。
vue2.5的源码中,macrotask降级的方案
setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。
在vue2.5的源码中,macrotask降级的方案依次是:
- setImmediate
- MessageChannel (对应的onmessage回调也是microtask,但也是个新API有兼容问题)
- setTimeout(最小时间间隔是约4ms)
- setImmediate是最理想的方案了(可惜的是只有IE和nodejs支持,有兼容问题) 所以最后的方案是setTimeout了.
5.源码分析
整体流程
watcher.js -> queueWatcher(this) -> nextTick(flushschedulerQueue) -> timeFunc -> flushCallbacks -> watcher.run()
src\core\observer\watcher.js
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
noop
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
export default class Watcher {
vm: Component;
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
...
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
...
}
cleanupDeps () {
...
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
//执行watcher的更新方法
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() //直接运行
} else {
queueWatcher(this) //加入队列
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
src\core\observer\scheduler.js
把批量执行watcher需要更新的update方法加入到队列里。
/* @flow */
import type Watcher from './watcher'
import config from '../config'
import { callHook, activateChildComponent } from '../instance/lifecycle'
import {
warn,
nextTick,
devtools,
inBrowser,
isIE
} from '../util/index'
export const MAX_UPDATE_COUNT = 100
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Reset the scheduler's state.
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
export let currentFlushTimestamp = 0
// Async edge case fix requires storing an event listener's attach timestamp.
let getNow: () => number = Date.now
if (inBrowser && !isIE) {
const performance = window.performance
if (
performance &&
typeof performance.now === 'function' &&
getNow() > document.createEvent('Event').timeStamp
) {
getNow = () => performance.now()
}
}
/**
* Flush both queues and run the watchers.
*/
//批量执行队列
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
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]
if (watcher.before) {
watcher.before()
}
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
}
}
}
// 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')
}
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
/**
* Queue a kept-alive component that was activated during patch.
* The queue will be processed after the entire tree has been patched.
*/
export function queueActivatedComponent (vm: Component) {
// setting _inactive to false here so that a render function can
// rely on checking whether it's in an inactive tree (e.g. router-view)
vm._inactive = false
activatedChildren.push(vm)
}
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
//添加watcher到队列里
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
- src\core\util\next-tick.js
- 用于使用哪种方式加入异步队列,微任务或 宏任务
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
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]()
}
}
//定义更新的方法
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {//优先使用promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {//针对ios特殊处理
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {//如果是node环境使用setImmediate
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout. 最后降级处理
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (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 => {
_resolve = resolve
})
}
}
5.总结:
- nextTick是在DOM更新完毕之后执行一个延迟的回调,在修改数据后可以立即获取最新dom内容。是vue的全局api。
- vue更新dom是异步进行的,只要监听到数据变化,Vue就会开启一个队列,并且缓冲同一事件循环中的所有数据变化。如果同一个watcher被调用多次,最终只会被推入队列一次。这种缓冲时,去除重复数据的操作,有效的减少不必要的计算与dom的实际操作。并且nextTick是插入到队列的最后面执行。所以能拿到最新的dom数据。
- 当我们需要在修改数据后想立即最新的dom,就需要使用nextTick方法
- nextTick包含两个参数一个是回调方法,一个是上下文。
- 在 Vue 生命周期的 created() 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick() 的回调函数中,因为created的时候dom还没有挂载。
- nextTick的实现原理,通过调用nextTick() 里面的方法。