Vue $nextTick 用途详解

Vue 中更新dom的逻辑是异步执行的,我们都知道js是单线程的,通常异步是通过回调函数的方式来实现的,所以在vue中我们如果想获取到数据改变后异步更新dom的最新内容,必须也通过回调的方式来获取。nextTick就是vue为我们提供的一个回调语法。看下面的例子:

<a>{{str}}</a>
data: {
  str: 'str1'
},
mounted () {
   this.str = 'str2'
   console.log(this.$el) // <a>str1</a>
   this.$nextTick(() => {
     console.log(this.$el) // <a>str2</a>
   })
}

从例子我们可以看出,在改变str后立马获取到的当前dom不是最新的,只有在nextTick里获取到的才是最新的。

源码解析
一:dom更新为何是异步的?
如果你了解vue响应式原理vue响应式原理 :我们知道vue的变量更新是通过 setter和setter方法来监控的,当数据更新的时候,执行如下步骤:
1.数据变动会触发set方法通知所有的观察者,看下面的代码中,notify这个方法就是通知底下的所有subs(注册在该值上的观察者集合)执行update操作。
2.update 调用queueWatcher函数
3.queueWatcher 执行的时候,同一个单线程循环周期内的观察者只加入队列一次(避免重复更新),加入完成后,在回调中开始执行队列(参考代码中的中文注释)
所以在单线程的一个执行周期中,反复的修改一个变量,在下一个周期里,只会按照最后一次改动更新一次dom,大大的提高了性能。

// 变量变化后通知观察者执行更新
 Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };
  // 观察者的原型链上的更新方法
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) { // 如果配置成同步的,直接更新
      this.run();
    } else {
      queueWatcher(this); // 默认是走这个方法异步执行
    }
  };
  function queueWatcher (watcher) {
    var id = watcher.id;
    // 这里保证同一个反复的改变的变量,观察者只加入队列一次
    if (has[id] == null) { 
      has[id] = true;
      if (!flushing) {// 如果这时候更新dom的操作还没开始 继续push
        queue.push(watcher);
      } else {
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        // 如果在异步渲染dom已经开始的时候再加入进来,顺序插入到上一次刚执行的位置,准备直接下一步执行
        while (i > index && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      // queue the flush
      // 变量锁,默认锁着保证在一个circle循环里的所有变量改变watcher都推到一个队列里
      if (!waiting) { 
        waiting = true;
        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);// 异步执行
      }
    }
  }
执行队列里的方法
/**
   * Flush both queues and run the watchers.
   */
  function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;
    queue.sort(function (a, b) { return a.id - b.id; });
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      if (watcher.before) {
        watcher.before();
      }
      id = watcher.id;
      has[id] = null; // 这里设为null让这个时候发生改变触发的观察者也能加入进来立即执行
      watcher.run();
      // in dev build, check and stop circular updates.
      if (has[id] != null) {
        circular[id] = (circular[id] || 0) + 1;
        // 限制一个观察者最大的更新次数在一个circle里
        if (circular[id] > MAX_UPDATE_COUNT) { 
          warn(
            'You may have an infinite update loop ' + (
              watcher.user
                ? ("in watcher with expression \"" + (watcher.expression) + "\"")
                : "in a component render function."
            ),
            watcher.vm
          );
          break
        }
      }
    }

    // keep copies of post queues before resetting state
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();

    resetSchedulerState();

    // call component updated and activated hooks
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
      devtools.emit('flush');
    }
  }

二:nextTick如何实现?

var timerFunc;
  // 方法一:promise 实现的异步
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
    // 方法二:MutationObserver 监听dom事件
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
    // 方法三:setImmediate
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Techinically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
  // 方法四:setTimeout
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

  function nextTick (cb, ctx) {
    var _resolve;
    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) {  // 返回一个promise
        _resolve = resolve;
      })
    }
  }

vue对于异步采用了如下几种机制来实现。
1.promise.then
2.MutationObserver dom事件监听
3.setImmediate 定时执行方法,会比setTimeout略早执行
4.setTimeout 传统的定时执行方法
vue通过特性探测,来选择用哪种方式来实现回调,同时还处理了一些设备的兼容特性,这其中几种方式的使用和区别,会在其他的文章中再做展开。

nextTick其他知识点:
nextTick 在浏览器原生支持promise的情况下,会返回一个promise对象
所以我们可以如下写法而避免嵌套过深。

this.$nextTick(()=>{
}).then(() => {
  // nextTick后的逻辑
})

我们也可以通过绑定this来改变nextTick里的作用域上下文

this.$nextTick(function(){
}).bind(obj)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

森哥的歌

一杯咖啡半包烟,助我熬过那长夜

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值