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字體

相關文章
相關標籤/搜索