Vue.nextTick浅析

<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> ```&lt;div id="app"&gt; &lt;button @click="add"&gt;add&lt;/button&gt; {{count}} &lt;ul ref="ul"&gt; &lt;li v-for="item in list"&gt; {{item}} &lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; ```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=&gt;{
        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(()=&gt;{
          let li = this.$refs.ul.querySelectorAll('li')
          li.forEach(item=&gt;{
          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 &lt; 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' &amp;&amp; isNative(setImmediate)) {
  macroTimerFunc = () =&gt; {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' &amp;&amp; (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () =&gt; {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () =&gt; {
    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' &amp;&amp; isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () =&gt; {
    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(() =&gt; {
    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 &amp;&amp; typeof Promise !== 'undefined') {
    return new Promise(resolve =&gt; {
      _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

来源:http://www.javashuo.com/article/p-frhhcyer-cc.html字体

相关文章
相关标签/搜索