Vue中有个API是nextTick
,官方文档是这样介绍做用的:html
将回调延迟到下次 DOM 更新循环以后执行。在修改数据以后当即使用它,而后等待 DOM 更新。
理解这部份内容,有助于理解Vue对页面的渲染过程,同时也能够了解到beforeUpdate
和updated
的使用。另外就是经过了解nextTick
的调用了解vue内部是如何使用Promise
的。这部份内容和以前介绍计算属性的内容也有关联,能够比照着看。vue
首先看一下我建立的例子:express
<!-- 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 }) } } })
当页面渲染完成,点击按钮触发事件以后,都会发生什么呢~~promise
直接介绍计算属性的时候说过,当页面初次加载渲染,会调用模板中的值,这时会触发该值的getter
设置。因此对于咱们这里,data中的name
和age
都会订阅updateComponent
这个方法,这里咱们看下这个函数的定义:dom
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
方法。async
/** * 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
的实例化的代码也贴出来:ide
// 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
的实现。oop
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
事件就介绍完了。