Vue nextTick[转]

Vue


原文引自 (opens new window)

# nextTick

在上一节的内容中,我们说到数据修改时会触发 setter 方法进行依赖的派发更新,而更新时会将每个 watcher 推到队列中,等待下一个 tick 到来时再执行 DOM 的渲染更新操作。这个就是异步更新的过程。

异步更新的好处:

由于 Vue 是数据驱动视图更新渲染,如果我们在一个操作中重复对一个响应式数据进行计算,例如 在一个循环中执行 this.num++ 一千次,由于响应式系统的存在,数据变化触发 setter,setter 触发依赖派发更新,更新调用 run 进行视图的重新渲染。这一次循环,视图渲染要执行一千次,很明显这是很浪费性能的,我们只需要关注最后第一千次在界面上更新的结果而已。所以利用异步更新显得格外重要。

# 基本实现

Vue 用一个 queue 收集依赖的执行,在下次微任务执行的时候统一执行 queue 中 Watcher 的 run 操作,与此同时,相同 id 的 watcher 不会重复添加到 queue 中,因此也不会重复执行多次的视图渲染。我们看 nextTick 的实现。

// 原型上定义的方法
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
}
// 构造函数上定义的方法
Vue.nextTick = nextTick
// 实际的定义
var callbacks = []
function nextTick(cb, ctx) {
  var _resolve
  // callbacks是维护微任务的数组。
  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()
  }
  // nextTick没有传递参数,且浏览器支持Promise,则返回一个promise对象
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

nextTick 定义为一个函数,使用方式为 Vue.nextTick( [callback, context] ),当 callback 经过 nextTick 封装后,callback 会在下一个 tick 中执行调用。从实现上,callbacks 是一个维护了需要在下一个 tick 中执行的任务的队列,它的每个元素都是需要执行的函数。pending 是判断是否在等待执行微任务队列的标志。而 timerFunc 是真正将任务队列推到微任务队列中的函数。我们看 timerFunc 的实现。

1.如果浏览器执行 Promise,那么默认以 Promsie 将执行过程推到微任务队列中。

var timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve()
  timerFunc = function () {
    p.then(flushCallbacks)
    // 手机端的兼容代码
    if (isIOS) {
      setTimeout(noop)
    }
  }
  // 使用微任务队列的标志
  isUsingMicroTask = true
}
1
2
3
4
5
6
7
8
9
10
11
12
13

flushCallbacks 是异步更新的函数,他会取出 callbacks 数组的每一个任务,执行任务,具体定义如下:

function flushCallbacks() {
  pending = false
  var copies = callbacks.slice(0)
  // 取出callbacks数组的每一个任务,执行任务
  callbacks.length = 0
  for (var i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
1
2
3
4
5
6
7
8
9

2.不支持 promise,支持 MutataionObserver

else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    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;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

3.如果不支持微任务方法,则会使用宏任务方法,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);
    };
  }
1
2
3
4
5
6
7
8

4.所有方法都不适合,会使用宏任务方法中的 setTimeout

else {
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
1
2
3
4
5

当 nextTick 不传递任何参数时,可以作为一个 promise 用,例如:

nextTick().then(() => {})
1

# 使用场景

说了这么多原理性的东西,回过头来看看 nextTick 的使用场景,由于异步更新的原理,我们在某一时间改变的数据并不会触发视图的更新,而是需要等下一个 tick 到来时才会更新视图,下面是一个典型场景:

<input v-if="show" type="text" ref="myInput" />
1
data() {
  show: false
},
mounted() {
  this.show = true;
  this.$refs.myInput.focus();// 报错
}
数据改变时,视图并不会同时改变,因此需要使用nextTick

mounted() {
  this.show = true;
  this.$nextTick(function() {
    this.$refs.myInput.focus();// 正常
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Last Updated: 10/21/2024, 4:15:17 PM