共计 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
事件就介绍完了。