咱们都知道vue是数据驱动视图,而vue中视图更新是异步的。在业务开发中,有没有经历过当改变了数据,视图却没有按照咱们的指望渲染?而须要将对应的操做放在nextTick中视图才能按照预期的渲染,有的时候nextTick也不能生效,而须要利用setTimeout来解决?html
搞清楚这些问题,那么就须要搞明白如下几个问题:
一、vue中究竟是如何来实现异步更新视图;
二、vue为何要异步更新视图;
三、nextTick的原理;
四、nextTick如何来解决数据改变视图不更新的问题的;
五、nextTick的使用场景。vue
如下分享个人思考过程。react
vue中每一个组件实例都对应一个 watcher实例,它会在组件渲染的过程当中把“接触”过的数据属性记录为依赖。以后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件从新渲染。
若是对vue视图渲染的思想还不是很清楚,能够参考这篇defineProperty实现视图渲染用defineProty模拟的Vue的渲染视图,来了解整个视图渲染的思想。git
可是Vue的视图渲染是异步的,异步的过程是数据改变不会当即更新视图,当数据所有修改完,最后再统一进行视图渲染。github
在渲染的过程当中,中间有一个对虚拟dom进行差别化的计算过程(diff算法),大量的修改带来频繁的虚拟dom差别化计算,从而致使渲染性能下降,异步渲染正是对视图渲染性能的优化。算法
/** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ 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 nextTick(flushSchedulerQueue) } } }
当第一次依赖有变化就会调用nextTick方法,将更新视图的回调设置成微任务或宏任务,而后后面依赖更新对应的watcher对象都只是被加入到队列中,只有当nextTick回调执行以后,才会遍历调用队列中的watcher对象中的更新方法更新视图。chrome
这个nextTick和咱们在业务中调用的this.$nextTick()是同一个函数。express
if (!waiting) { waiting = true nextTick(flushSchedulerQueue) }
flushSchedulerQueue刷新队列的函数,用于更新视图segmentfault
function flushSchedulerQueue () { 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. 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] 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 } } }
那么nextTick究竟是个什么东西呢?数组
vue 2.5中nextTick的源码以下(也能够跳过源码直接看后面的demo,来理解nextTick的用处):
/** * Defer a task to execute it asynchronously. */ export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // An asynchronous deferring mechanism. // In pre 2.4, we used to use microtasks (Promise/MutationObserver) // but microtasks actually has too high a priority and fires in between // supposedly sequential events (e.g. #4521, #6690) or even between // bubbling of the same event (#6566). Technically setImmediate should be // the ideal choice, but it's not available everywhere; and the only polyfill // that consistently queues the callback after all DOM events triggered in the // same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } } return function queueNextTick (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, reject) => { _resolve = resolve }) } } })()
用下面这个demo来感觉依赖更新时和nextTick的关系以及nextTick的用处:
function isNative(Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } const nextTick = (function () { let pending = false; let callbacks = [] let timerFunc function nextTickHandler() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } } console.log('timerFunc:', timerFunc) return function queueNextTick(cb, ctx) { callbacks.push(() => { if (cb) { cb.call(ctx) } }) // console.log('callbacks:', callbacks) if (!pending) { pending = true console.log('pending...', true) timerFunc() } } })() // 模拟异步视图更新 // 第一次先将对应新值添加到一个数组中,而后调用一次nextTick,将读取数据的回调做为nextTick的参数 // 后面的新值直接添加到数组中 console.time() let arr = [] arr.push(99999999) nextTick(() => { console.log('nextTick one:', arr, arr.length) }) function add(len) { for (let i = 0; i < len; i++) { arr.push(i) console.log('i:', i) } } add(4) // console.timeEnd() // add() // add() nextTick(() => { arr.push(888888) console.log('nextTick two:', arr, arr.length) }) add(8)的值以后 console.timeEnd()
在chrome运行结果以下:
能够看到第二个nextTick中push的值最后渲染在add(8)的值以后,这也就是nextTick的做用了,nextTick的做用就是用来处理须要在数据更新(在vue中手动调用nextTick时对应的是dom更新完成后)完才执行的操做。
nextTick的原理:
首先nextTick会将外部传进的函数回调存在内部数组中,nextTick内部有一个用来遍历这个内部数组的函数nextTickHandler,而这个函数的执行是异步的,何时执行取决于这个函数是属于什么类型的异步任务:微任务or宏任务。
主线程执行完,就会去任务队列中取任务到主线程中执行,任务队列中包含了微任务和宏任务,首先会取微任务,微任务执行完就会取宏任务执行,依此循环。nextTickHandler设置成微任务或宏任务就能保证其老是在数据修改完或者dom更新完而后再执行。(js执行机制能够看promise时序问题&js执行机制)
为何vue中对设置函数nextTickHandler的异步任务类型会有以下几种判断?
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } }
浏览器环境中常见的异步任务种类,按照优先级:
macro task
:同步代码、setImmediate
、MessageChannel
、setTimeout/setInterval
micro task
:Promise.then
、MutationObserver
而为何最后才判断使用setTimeout?
vue中目的就是要尽量的快地执行回调渲染视图,而setTimeout有最小延迟限制:若是嵌套深度超过5级,setTimeout(回调,0)就会有4ms的延迟。
因此首先选用执行更快的setImmediate,可是setImmediate有兼容性问题,目前只支持Edge、Ie浏览器:
能够用一样执行比setTimeout更快的宏任务MessageChannel来代替setImmediate。MessageChannel兼容性以下:
当以上都不支持的时候,就使用new Promise().then(),将回调设置成微任务,Promise不支持才使用setTimeout。
总结:
nextTick就是利用了js机制执行任务的规则,将nextTick的回调函数设置成宏任务或微任务来达到在主线程的操做执行完,再执行的目的。
在vue中主要提供对依赖Dom更新完成后再作操做的状况的支持
当改变数据,视图没有按预期渲染时;都应该考虑是不是由于本须要在dom执行完再执行,然而实际却在dom没有执行完就执行了代码,若是是就考虑使用将逻辑放到nextTick中,有的时候业务操做复杂,有些操做可能须要更晚一些执行,放在nextTick中仍然没有达到预期效果,这个时候能够考虑使用setTimeout,将逻辑放到宏任务中。
基于以上分析,能够列举几个nextTick经常使用到的使用场景:
// input 定位 scrollToInputBottom() { this.$nextTick(() => { this.$refs.accept_buddy_left.scrollTop = this.$refs.accept_buddy_left.scrollTop + 135 this.$refs.accept_buddy_ipt[ this.$refs.accept_buddy_ipt.length - 1 ].$refs.ipt.focus() }) },
// 监听来自 url 的期数变化,跳到该期数 urlInfoTerm: { immediate: true, handler(val) { if (val !== 0) { this.$nextTick(function() { // 计算期数所在位置的高度 this.setCellsHeight() //设置滚动距离 this.spaceLenght = this.getColumnPositionIndex( this.list, ) setTimeout(() => { this.setScrollPosition(val) }, 800) }) } },