前言
相信对于每一个使用Vue的前端同学来说,nextTick这个方法并不陌生,作用就是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
在开始往下阅读之前,如果有对JS浏览器的事件循环机制不清楚的同学,可以点击这里进行了解。此外,希望大家能够对下图有足够的认识,以便理解nextTick运行机制。以vue@2.6.x版本进行分析
在进入我们今天的主角nextTick讲解之前,我们先简单地介绍一下关于nextTick执行的整体流程,以便大家能够更好地从整体上去把握、理解。(以下的代码模块,都只截取了跟nextTick相关的核心部分)
Vue初始化
在Vue初始化过程中,会执行原型上的_init方法,进行一系列的初始化操作,最后会调用$mount方法。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
... // 不关注的部分暂缺忽略掉
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
通过调用$mount方法,最后会调用mountComponent方法,有关代码如下
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
return vm
}
执行mountComponent方法中,可以直接看到,会对updateComponent方法进行赋值,这个方法用于虚拟DOM映射成真实DOM,视图的更新,我们暂且知道这一点就行了。另一个核心的函数就是执行了newWatcher,而mountComponent会作为一个参数传入Watcher中,用于首次渲染和之后的派发更新时触发的重新渲染,且每一个Vue实例会有拥有一个唯一的渲染Watcher。我们接下来看看Watcher的相关实现
Watcher
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
// ...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
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)
}
}
}
}
// ...
}
我们主要关注get、run、update方法,其余不重要的我们暂且不关注,对于我们当前的渲染Watcher来说,get方法其实就是updateComponent方法,run方法其实就是执行一遍get方法, update方法我们之后再结合例子进行讨论。
DefineReactive
vue响应式的核心代码如下,相关代码
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ....
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ....
}
return value
},
set: function reactiveSetter (newVal) {
// ....
dep.notify()
}
})
}
defineReactive中,会重写每一个key对应的get与set方法,且每一个key会维护一个唯一的Dpe实例,那什么时候进行依赖收集了呢?还记得我上面说到在mountComponent中会执行new Watcher(....)吗?
其实在new Watcher的过程中就会取执行传入updateComponent方法 收集依赖: 这个时候就会在执行render函数生成虚拟DOM时,读取到数据,即触发了get操作,会用过Watcher的addDpe方法,把当前的渲染Watcher加入到了每个key值对应Dep实例下的subs数组中。
派发更新: 另外,再改变数据的值时,就会触发set函数,就会遍历Dpe实例下的subs数组,执行每一个watcher的update方法,关于Dpe的核心代码如下:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
// ...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
相信到这里,大家应该对vue如何初初始化及更新视图及Watcher及Dep的作用有了一定的了解了,接下来我们来着重分析一下nextTick源码入口
/* @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)) {
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]'
)) {
// 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)) {
// 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
})
}
}
我们只分析传入fn的情况,返回Promise的情况类似,大家可以自行分析。 首先nextTick的定义开始分析,nextTick可以接受一个回调函数fn,并且每次执行时会通过在外包裹一层匿名函数的方式push到callbacks的任务队列中,接下来判断pending的值,如果pending为false,则会执行timerFunc,从代码中可以看到timerFunc实现的优先级为Promise.then > MutationObserver > setImmediate > setTimeout,我们假设timerFunc基于Promise.then实现,来进行分析。
测试代码
<template>
<div @click="onClick" ref="target">{{ name }}</div>
</template>
<script>
export default {
data () {
return {
name: 'small-zsj'
}
},
methods: {
onClick () {
this.name = 'big-zsj' // 同步任务
console.log(this.$refs.target.innerText) // 'small-zsj'
this.$nextTick(() => {
console.log(this.$refs.target.innerText) // 'big-zsj' 更新之后的DOM
})
}
}
}
</script>
}
当我们点击目标元素的时候,触发了onClick事件,随后修改了this.name的值,这个时候就会触发name的setter,接着会执行 dep.notify() -> watcher(渲染).update -> queueWatcher,接下来我们来看下queueWatcher这个函数的实现
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
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)
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
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
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')
}
}
}
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 */)
}
}
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)
}
}
}
从代码中可以看到,会对每个watcher的id进行判断,如果已经存在了就直接return,比如循环1000次加1的操作,修改数据,vue并不会把setter->dep.notify()->watcher.update->watcher.run执行1000次,因为有id的判断,所以只需要执行一次就够了,这在性能上是一个很大的提升,最后代码执行之后会调用nextTick(flushSchedulerQueue),其实就是把更新视图的操作的函数作为参数传入了nextTick中(异步队列),把flushSchedulerQueue也push到了callbacks数组中。相当于执行了如下代码:
<template>
<div @click="onClick" ref="target">{{ name }}</div>
</template>
<script>
export default {
data () {
return {
name: 'small-zsj'
}
},
methods: {
onClick () {
this.$nextTick(() => {
console.log(this.$refs.target.innerText) // 'small-zsj'
// 因为callbacks先push了当前函数,但是更新DOM的操作是在this.name = 'big-zsj'之后
})
this.name = 'big-zsj' // 同步任务
this.$nextTick(() => {
// 更新DOM,执行watcher.run()
})
console.log(this.$refs.target.innerText) // 'small-zsj'
this.$nextTick(() => {
console.log(this.$refs.target.innerText) // 'big-zsj' 更新之后的DOM
})
}
}
}
</script>
}
因为nextTick是就Promise.then实现的,所以会在同步任务结束后,从微任务队列里面去遍历执行,只要保证nextTick是在数据改变后执行,那么拿到的DOM就是更新后的DOM(因为callbacks队列是顺序执行的)。
注意,DOM更新跟视图渲染不是同一个概念,如图,在JS引擎执行完微队列的所有任务后,浏览器会将JS进程挂起,此时渲染进程会开始工作,对视图进行更新渲染,所以DOM的更新操作应尽可能的放在微队列中进行批量处理,提高效率。
以上便是我对nextTick的一些理解,如有不足或者不对的地方,还望指出!!