Vue.nextTick浅析

Vue.nextTick 浅析

Vue 的特点之一就是响应式,但数据更新时,DOM 并不会立即更新。当我们有一个业务场景,需要在 DOM 更新之后再执行一段代码时,可以借助nextTick实现。以下是来自官方文档的介绍:

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

具体的使用场景和底层代码实现在后面的段落说明和解释。

用途

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

前者是全局方法,可以显式指定执行上下文,而后者是实例方法,执行时自动绑定this到当前实例上。

此外,在 2.1.0 版本还新增了不传入回调的使用方式,这种调用会返回一个 Promise,在 then 的回调执行目标操作即可,如vm.$nextTick().then(cb)

以下是一个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的回调中执行更新样式的操作。

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 更新后才执行代码。

源码

/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清空回调队列,最后使用循环执行在副本里的所有函数。

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

    原文作者:荡漾
    原文地址: https://segmentfault.com/a/1190000016495892
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞