Vue.nextTick淺析

Vue.nextTick 淺析

Vue 的特色之一就是響應式,但數據更新時,DOM 並不會當即更新。當咱們有一個業務場景,須要在 DOM 更新以後再執行一段代碼時,能夠藉助nextTick實現。如下是來自官方文檔的介紹:html

將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。

具體的使用場景和底層代碼實如今後面的段落說明和解釋。api

用途

Vue.nextTick([callback, context])vm.$nextTick([callback])數組

前者是全局方法,能夠顯式指定執行上下文,然後者是實例方法,執行時自動綁定this到當前實例上。瀏覽器

此外,在 2.1.0 版本還新增了不傳入回調的使用方式,這種調用會返回一個 Promise,在 then 的回調執行目標操做便可,如vm.$nextTick().then(cb)app

如下是一個nextTick使用例子:函數

<div id="app">
  <button @click="add">add</button>
  {{count}}
  <ul ref="ul">
    <li v-for="item in list">
      {{item}}
    </li>
  </ul>
</div>
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";
      });
    }
  }
});

以上的代碼,指望在每次新增一個列表項時都使得列表項的字體是紅色的,但實際上新增的列表項字體還是黑色的。儘管data已經更新,但新增的 li 元素並不當即插入到 DOM 中。若是但願在 DOM 更新後再更新樣式,能夠在nextTick的回調中執行更新樣式的操做。oop

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";
        });
      });
    }
  }
});

解釋

數據更新時,並不會當即更新 DOM。若是在更新數據以後的代碼執行另外一段代碼,有可能達不到預想效果。將視圖更新後的操做放在nextTick的回調中執行,其底層經過微任務的方式執行回調,能夠保證 DOM 更新後才執行代碼。post

源碼

/src/core/instance/index.js,執行方法renderMixin(Vue)Vue.prototype添加了$nextTick方法。實際在Vue.prototype.$nextTick中,執行了nextTick(fn, this),這也是vm.$nextTick( [callback] )自動綁定this到執行上下文的緣由。字體

nextTick函數在/scr/core/util/next-tick.js聲明。在next-tick.js內,使用數組callbacks保存回調函數,pending表示當前狀態,使用函數flushCallbacks來執行回調隊列。在該方法內,先經過slice(0)保存了回調隊列的一個副本,經過設置callbacks.length = 0清空回調隊列,最後使用循環執行在副本里的全部函數。this

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]();
  }
}

接着定義函數marcoTimerFuncmicroTimerFunc

先判斷是否支持setImmediate,若是支持,使用setImmediate執行回調隊列;若是不支持,判斷是否支持MessageChannel,支持時,在port1監聽message,將flushCallbacks做爲回調;若是仍不支持MessageChannel,使用setTimeout(flushCallbacks, 0)執行回調隊列。無論使用哪一種方式,macroTimerFunc最終目的都是在一個宏任務裏執行回調隊列。

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);
  };
}

而後判斷是否支持Promise,支持時,新建一個狀態爲resolvedPromise對象,並在then回調裏執行回調隊列,如此,便在一個微任務中執行回調,在 IOS 的 UIWebViews 組件中,儘管能建立一個微任務,但這個隊列並不會執行,除非瀏覽器須要執行其餘任務;因此使用setTimeout添加一個不執行任何操做的回調,使得微任務隊列被執行。若是不支持Promise,使用降級方案,將microTimerFunc指向macroTimerFunc

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;
}

在函數nextTick內,先將函數cb使用箭頭函數包裝起來並添加到回調隊列callbacks,轉入的回調cb會在callbacks被遍歷執行的時候執行。若是沒有傳入cb,則是形如this.$nextTick().then(cb)的使用方式,因此要返回一個fulfilled的 Promise,在箭頭函數內則須要執行resolved,令返回的 Promise 狀態變爲fulfilled。接着判斷當前是否正在執行回調,若是不是,將pengding設置爲真。判斷回調執行是宏任務仍是微任務,分別經過marcoTimerFuncmicroTimerFunc來觸發回調隊列。最後,若是,沒有傳入cb,則須要建立一個Promise實例並返回以支持鏈式調用,而且將_resolve指向返回 Promise 的resolve函數。

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;
    });
  }
}

而全局方法Vue.nextTick/src/core/global-api/index.js中聲明,是對函數nextTick的引用,因此使用時能夠顯式指定執行上下文。

Vue.nextTick = nextTick;

小結

本文關於nextTick的使用場景和源碼作了簡單的介紹,若是想深刻了解這部分的知識,能夠去了解一下微任務mircotask和宏任務marcotask

相關文章
相關標籤/搜索