Vue 的 nextTick 与 promise then 的顺序问题

qaq一个懒人
  • 31

image.png

打印: 1 2 promise 3


image.png

打印: 1 promise 2 3

vue中的$nextTick不是也用Promise().resolve().then()做的吗?都是微任务,为什么在第一张图中, '2'比'Promise'先被打印出来?

回复
阅读 5.4k
6 个回答

nextTick支持两种形式,一种是回调,一种是promise,具体哪种取决于你有没有传入回调函数,看下面vue源码
nextTick

当你传入回调函数时vue实际上是将回调推入一个队列等待下次更新时机时一起调用,然后因为你第一个例子中修改了data的msg,所以会触发vue 的update逻辑,也就触发了nextTick,你可以看下vue的调用栈
call stack

可以看到update后就紧接着执行了nextTick来执行推入的回调函数,所以才会出现第一个例子中的那样,如果你改成this.$nextTick().then(...)这才是promise的形式,这样输出才会符合你所预期的,也就是第二个例子那样

有意思哈,还特意去查了下文档:
Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有"then" 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
$nextTick:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新
微任务?两者的使用场景不一样,执行的先后无法排列,鄙人如此理解。

promise是js中的异步控制, nextTick是Vue的回调.当改变dom后触发
也就是this.msg = 'end'当msg被渲染为end后触发nextTick

其实我比较好奇 在第二张图里面 mounted方法没有操作与dom相关的数据,为什么$nextTick还能执行.

Azilla
  • 2
新手上路,请多包涵

你能提出这个问题说明你应该阅读过源码,知道nextTick在native Promise的时候是基于Promise实现的。那你应该也知道通过Promise加入微任务的实际上是一个遍历调用回调函数数组的回调函数,这个函数只会同时在微任务队列中出现一次。而我们传入的函数被存放在回调组中。这样这个问题就很简单了。
你所遇到的这个问题只会出现在生命周期函数中,而不会出现在一般的methods中定义的函数中,除非你在生命周期函数中调用它。
因为当你上述例子中的mounted钩子执行到nextTick时,仍然执行的是同步代码,组件元素即将完成挂载到父元素。此时你不能保证nextTick的回调函数没有已经被之前的一些其他代码加入到微任务队列。
你遇到的这个问题,正好是因为nextTick的回调函数已经被加入到微任务队列了,而你传入的回调仅仅被push到回调数组中,因此它会先于promise.then的回调被调用。
我猜测之后你可能删除了一行代码,然后保存了代码编辑器。这时你并没有刷新页面,先于这个mounted将nextTick回调函数加入微任务队列的一些代码可能没有再次执行,也就打印出了我们一般期望的结果。因此你可以尝试每次都刷新页面,这样你得到的结果应该每次都是一样的。

蓝胖子
  • 2
新手上路,请多包涵

1先执行不用说了吧 定时器宏任务放在队列后执行 .then 没有nextTicku优先级高 上后面呆着去 然后在执行定时器

首先我们要知道nextTick方法做了什么?直接上源码:

function nextTick (cb, ctx) {
  var _resolve;
  // 包装cb回调函数,如果没有回调则返回一个promise实例
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

主要做了两件事:

  • 把回调函数添加到callbacks回调队列中
  • 执行timerFunc函数
    timerFunc函数是什么?看源码:

    var callbacks = [];
    var pending = false;
    
    function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
    }
    var timerFunc;
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      if (isIOS) { setTimeout(noop); }
    };
    }
    // 省略优雅降级使用setTimeout的部分代码

    timerFunc就是把callbacks回调队列的执行放到了p.then()中。

好了知道了nextTicktimerFunc是干什么的之后,我们再回到题目。

当执行到this.msg = 'end'时,触发了msg属性的setter,setter中执行dep.notify()派发更新,触发Watcher的update方法,最终会执行nextTick(flushSchedulerQueue)(这里可以在第一张图的nextTick回调中加个断点就执行函数调用栈了)。

也就是说,执行完this.msg='end'的时候,会触发一次nextTick,继而执行timerFunc方法,然后会把p.then(flushCallbacks)推到微任务中,然后再接着执行Promise.resolve().then(...),再继续走,后面的this.$nextTick(...)只是把回调函数收集到了callbacks

因此第一张图中2是比promise!先执行。

宣传栏