<h1>Vue.nextTick浅析</h1> <p>Vue的特色之一就是响应式,但数据更新时,DOM并不会当即更新。当咱们有一个业务场景,须要在DOM更新以后再执行一段代码时,能够借助<code>nextTick</code>实现。如下是来自官方文档的介绍:</p> <blockquote>将回调延迟到下次 DOM 更新循环以后执行。在修改数据以后当即使用它,而后等待 DOM 更新。</blockquote> <p>具体的使用场景和底层代码实如今后面的段落说明和解释。</p> <h2>用途</h2> <p><code>Vue.nextTick( [callback, context] )</code> 与 <code>vm.$nextTick( [callback] )</code></p> <p>前者是全局方法,能够显式指定执行上下文,然后者是实例方法,执行时自动绑定<code>this</code>到当前实例上。</p> <p>如下是一个<code>nextTick</code>使用例子:</p> ```<div id="app"> <button @click="add">add</button> {{count}} <ul ref="ul"> <li v-for="item in list"> {{item}} </li> </ul> </div> ```segmentfault
new Vue({ el: '#app', data: { count: 0, list: [] }, methods:{ add() { this.count += 1 this.list.push(1) let li = this.$refs.ul.querySelectorAll('li') li.forEach(item=>{ item.style.color = 'red'; }) } } })
<p>以上的代码,指望在每次新增一个列表项时都使得列表项的字体是红色的,但实际上新增的列表项字体还是黑色的。尽管<code>data</code>已经更新,但新增的li元素并不当即插入到DOM中。若是但愿在DOM更新后再更新样式,能够在<code>nextTick</code>的回调中执行更新样式的操做。</p>api
new Vue({ el: '#app', data: { count: 0, list: [] }, methods:{ add() { this.count += 1 this.list.push(1) this.$nextTick(()=>{ let li = this.$refs.ul.querySelectorAll('li') li.forEach(item=>{ item.style.color = 'red'; }) }) } } })
<h3>解释</h3> <p>数据更新时,并不会当即更新DOM。若是在更新数据以后的代码执行另外一段代码,有可能达不到预想效果。将视图更新后的操做放在<code>nextTick</code>的回调中执行,其底层经过微任务的方式执行回调,能够保证DOM更新后才执行代码。</p> <h2>源码</h2> <p>在<code>/src/core/instance/index.js</code>,执行方法<code>renderMixin(Vue)</code>为<code>Vue.prototype</code>添加了<code>$nextTick</code>方法。实际在<code>Vue.prototype.$nextTick</code>中,执行了<code>nextTick(fn, this)</code>,这也是<code>vm.$nextTick( [callback] )</code>自动绑定<code>this</code>到执行上下文的缘由。</p> <p><code>nextTick</code>函数在<code>/scr/core/util/next-tick.js</code>声明。在<code>next-tick.js</code>内,使用数组<code>callbacks</code>保存回调函数,<code>pending</code>表示当前状态,使用函数<code>flushCallbacks</code>来执行回调队列。在该方法内,先经过<code>slice(0)</code>保存了回调队列的一个副本,经过设置<code>callbacks.length = 0</code>清空回调队列,最后使用循环执行在副本里的全部函数。</p>数组
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]() } }
<p>接着定义函数<code>marcoTimerFunc</code>、<code>microTimerFunc</code>。</p> <p>先判断是否支持<code>setImmediate</code>,若是支持,使用<code>setImmediate</code>执行回调队列;若是不支持,判断是否支持<code>MessageChannel</code>,支持时,在<code>port1</code>监听<code>message</code>,将<code>flushCallbacks</code>做为回调;若是仍不支持<code>MessageChannel</code>,使用<code>setTimeout(flushCallbacks, 0)</code>执行回调队列。无论使用哪一种方式,<code>macroTimerFunc</code>最终目的都是在一个宏任务里执行回调队列。</p>浏览器
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } }
<p>而后判断是否支持<code>Promise</code>,支持时,新建一个状态为<code>resolved</code>的<code>Promise</code>对象,并在<code>then</code>回调里执行回调队列,如此,便在一个微任务中执行回调,在IOS的UIWebViews组件中,尽管能建立一个微任务,但这个队列并不会执行,除非浏览器须要执行其余任务;因此使用<code>setTimeout</code>添加一个不执行任何操做的回调,使得微任务队列被执行。若是不支持<code>Promise</code>,使用降级方案,将<code>microTimerFunc</code>指向<code>macroTimerFunc</code>。</p>app
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { 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) } } else { // fallback to macro microTimerFunc = macroTimerFunc }
<p>在函数<code>nextTick</code>内,先将函数<code>cb</code>使用箭头函数包装起来并添加到回调队列<code>callbacks</code>。接着判断当前是否正在执行回调,若是不是,将<code>pengding</code>设置为真。判断回调执行是宏任务仍是微任务,分别经过<code>marcoTimerFunc</code>、<code>microTimerFunc</code>来触发回调队列。最后返回一个<code>Promise</code>实例以支持链式调用。</p>函数
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 if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
<p>而全局方法<code>Vue.nextTick</code>在<code>/src/core/global-api/index.js</code>中声明,是对函数<code>nextTick</code>的引用,因此使用时能够显示指定执行上下文。</p>oop
Vue.nextTick = nextTick
<h2>小结</h2> <p>本文关于<code>nextTick</code>的使用场景和源码作了简单的介绍,若是想深刻了解这部分的知识,能够去了解一下微任务<code>mircotask</code>和宏任务<code>marcotask</code>。</p>post