共计 7814 个字符,预计需要花费 20 分钟才能阅读完成。
Vue 中有个 API 是nextTick
, 官方文档是这样介绍作用的:
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
理解这部分内容,有助于理解 Vue 对页面的渲染过程,同时也可以了解到 beforeUpdate
和updated
的使用。另外就是通过了解 nextTick
的调用了解 vue 内部是如何使用 Promise
的。这部分内容和之前介绍计算属性的内容也有关联,可以比照着看。
首先看一下我创建的例子:
<!-- HTML 部分 -->
<div id="test">
<p>{{name}}的年龄是{{age}}</p>
<!-- <p>
{{info}}
</p> -->
<div> 体重 <input type="text" v-model="age" /></div>
<button @click="setAge"> 设置年龄为 100</button>
</div>
// js 部分
new Vue({
el: '#test',
data() {
return {
name: 'tuanzi',
age: 2
}
},
beforeUpdate() {console.log('before update')
debugger
},
updated() {console.log('updated')
debugger
},
methods: {setAge() {
this.age = 190
debugger
this.$nextTick(() => {console.log('next tick', this.age)
debugger
})
}
}
})
当页面渲染完成,点击按钮触发事件之后,都会发生什么呢~~
直接介绍计算属性的时候说过,当页面初次加载渲染,会调用模板中的值,这时会触发该值的 getter
设置。所以对于我们这里,data 中的 name
和age
都会订阅 updateComponent
这个方法,这里我们看下这个函数的定义:
updateComponent = () => {vm._update(vm._render(), hydrating)
}
简而言之,这时用来渲染页面的,所以当代码执行到 this.age = 190
,这里就会触发age
的setter
属性,该属性会调用 dep.notify
方法:
// 通知
notify() {
// stabilize the subscriber list first
// 浅拷贝订阅列表
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 关闭异步,则 subs 不在调度中排序
// 为了保证他们能正确的执行,现在就带他们进行排序
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}
这里的 this.subs
就是页面初始化过程中,age
这个属性收集到的依赖关系,也就是 renderWatcher
实例。接着调用 renderWatcher
的update
方法。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
// debugger
/* istanbul ignore else */
if (this.lazy) {
// 执行 computedWacher 会运行到这里
this.dirty = true
} else if (this.sync) {this.run()
} else {
// 运行 renderWatcher
queueWatcher(this)
}
}
那为了更好的理解这里,我把 renderWatcher
的实例化的代码也贴出来:
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
{before() {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
因此,renderWatcher
是没有设置 lazy
这个属性的,同时我也没有手动设置 sync
属性,因此代码会执行到 queueWatcher(this)
。注意这里的this
,当前属于renderWatcher
实例对象,因此这里传递的 this 就是该对象。
// 将一个 watcher 实例推入队列准备执行
// 如果队列中存在相同的 watcher 则跳过这个 watcher
// 除非队列正在刷新
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
debugger
if (has[id] == null) {has[id] = true
if (!flushing) {
// 没有在刷新队列,则推入新的 watcher 实例
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.
// 队列已经刷新,则用传入的 watcher 实例的 id 和队列中的 id 比较,按大小顺序插入队列
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
}
debugger
nextTick(flushSchedulerQueue)
}
}
}
这段代码比较简单,就说一点。代码里有个判断是 config.async
,这是 Vue 私有对象上的值,默认的是true
,因此代码会执行到nextTick
这里,此时会传入一个回调函数 flushSchedulerQueue
,我们这里先不说,之后用的的时候再介绍。现在看看nextTick
的实现。
const callbacks = []
let pending = false
export function nextTick(cb?: Function, ctx?: Object) {
debugger
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})
}
}
pendding
用来判断是否存在等待的队列,callbacks
是执行回调的队列。那对于此时此刻,就是向 callbacks
推入一个回调函数,其中要执行的部分就是flushSchedulerQueue
。因为是初次调用这个函数,这里的就会调用到timerFunc
。
let timerFunc
const p = Promise.resolve()
timerFunc = () => {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)
}
现在毫无因为的是 timerFunc
这个函数会被调用。但是有个问题,p.then(flushCallbacks)
这句话会执行么?来看个例子:
function callback() {console.log('callback')
}
let p = Promise.resolve()
function func() {p.then(callback)
}
console.log('this is start')
func()
console.log('this is pre promise 1')
let a = 1
console.log('this is pre promise 2')
console.log(a)
思考一下结果是什么吧。看看和答案是否一致:
说回上面,p.then(flushCallbacks)
这句话在这里会执行,但是是将 flushCallbacks
这个方法推入了微任务队列,要等其他的同步代码执行完成,执行栈空了之后才会调用。所以对于 renderWatcher
来说,目前就算执行完了。
接下来代码执行到这里:
this.$nextTick(() => {console.log('next tick', this.age)
debugger
})
看下 $nextTick
的定义:
Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)
}
这里定义 $nextTick
是定义在 Vue 的原型对象上,所以在页面中可以通过 this.$nextTick
调用,同时传入的 this
就是当前页的实例。所以看会 nextTick
定义的部分,唯一的区别是,这是的 pendding
是false
,因此不会再调用一次timerFunc
。
setAge
里的同步代码都执行完了,因此就轮到 flushCallbacks
出场。来看下定义:
function flushCallbacks() {
debugger
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
console.log(copies)
for (let i = 0; i < copies.length; i++) {copies[i]()}
}
这里定义的位置和定义 nextTick
是在同一个文件里,因此 pendding
和callbacks
是共享的。主要就看 copies[i]()
这一段。经过前面的执行,此时 callbacks.length
的值应该是 2。copies[1]
指的就是先前推进队列的flushSchedulerQueue
。
/**
* Flush both queues and run the watchers.
*
* 刷新队列并且运行 watcher
*/
function flushSchedulerQueue() {currentFlushTimestamp = getNow()
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.
// 给刷新队列排序,原因如下:// 1. 组件的更新是从父组件开始,子组件结束
// 2. 组件的 userWatcher 的运行总是先于 renderWatcher
// 3. 如果父组件的 watcher 运行期间,子组件被销毁了,后续运行可以跳过被销毁的子组件
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
}
}
}
watcher.before
这个方法是存在的,先前的代码中有,在初始化 renderWatcher
时传入了这个参数。这里就调用了 callHook(vm, 'beforeUpdate')
,所以能看出来,此时beforeUpdate
执行了。接着执行 watcher.run()
。run
是Watcher
类上定义的一个方法。
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
debugger
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)
}
}
}
}
this.active
初始化的值就是 true,get
方法之前的文章也提到过,这里再贴一遍代码:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// debugger
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
}
这部分代码之前说过,这里就不再说了,只提一点,此时的 this.getter
执行的是 updateComponent
, 其实也就是里面定义的vm._update(vm._render(), hydrating)
。关于render
和update
我会在分析虚拟 dom 时介绍。
现在需要知道的是,页面此时会重新渲染,我在 setAge
方法中修改了 age
的值,当 vm._update
执行完,就会发现页面上的值变化了。那接着就执行 callbacks
中的下一个值,也就是我写在 $nextTick
中的回调函数,这个就很简单,没必要再说。点击按钮到现在新的页面渲染完成,执行的结果就是:
before update
updated
next tick 100
这里就把整个流程讲完了,但是我想到 vue 文档中说的:
在修改数据之后立即使用它,然后等待 DOM 更新
假设我现在要是把 $nextTick
放到修改值之前呢。把 setAge
修改一下。
setAge() {this.$nextTick(() => {console.log('next tick', this.age)
debugger
})
debugger
this.age = 100
}
思考一下,此时点击按钮,页面会打印出什么东西。按照逻辑,因为 $nextTick
写在了前面,因此会被先推进 callbacks
中,也就会被第一个执行。所以此时我以为打印出来的 age
还是 2。但我既然都这样说了,那结果肯定是和我以为的不一样,但我有一部分想的没错,就是优先推入,优先调用。当我忘了一点,大家也可以会想一下,renderWatcher
是如何被触发的?
$nextTick
回调现在是进入了微任务队列,所以会继续执行接下来的赋值。此时会触发 age
设置的 setter
里的 dep.notify
。但在调用之前,新的值就已经传给 age 了。所以当$nextTick
里的回调执行时,会触发 age
的getter
,拿到的值就是新的值。
整个 nextTick
事件就介绍完了。