前端面试系列【015】 - nextTick 是什么?它是干什么的?实现原理是什么?

1,032 阅读1分钟

对于 nextTick,相信大家都不陌生,比如下面这段代码,应该会经常被用到:

mounted () {
  this.$nextTick(()=>{
		/* do something */
  })
}

但这里的 $nextTick 具体有什么作用,又是为什么需要它呢?对于这个问题,部分开发人员可能就不求甚解了。

要回答上面的问题,我们先来看个例子:

<div id="root">
  <p id="msg">{{ msg }}</p>
</div>
<script>
	const app = new Vue({
    el: '#root',
    data: { msg: 'ready~~' },
    mounted() {
      this.msg= Math.random()
      console.log('1', this.msg)
      console.log('1 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('2', this.msg)
      console.log('2 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('3', this.msg)
      console.log('3 innerHTML:' + msg.innerHTML)
      this.$nextTick(() => {
        console.log('nextTick innerHTML:' + msg.innerHTML)
      })
    }
	});
</script>

如果不知道 vue 的异步更新,这个问题很可能回答错误。

关于 vue 异步更新,可以看看这篇文章:Vue 异步更新策略

事实上,在 vue 中,一个值在同一执行流程内多次更新,视图只会渲染一次。了解了这一点,上面的问题就很清楚了,我们看看输出来验证一下吧:

果然,值发生改变,但 DOM 中的值没有随之改变,而在 nextTick 中却能拿到更新后的 DOM。

到这里,已经大致明白了 nextTick 的作用了,接下来我们看看官方的说明:

Vue.nextTick( [callback, context] ):在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

原理

想要知道 nextTick 的原理,我们先来看看它的源码:

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
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve
    })
  }
}

可以看到,它会将用户传递的回调函数 cb 入队,然后启动时间函数,而这个时间函数会根据环境选择合适的异步方法调用 flushCallbacks

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

然后在这里,会遍历执行所有的回调,最后在回调中执行 watcher.run() 以更新视图。

从上面的代码我们可以知道,当 nextTick 中的回调执行的时候,视图已经更新了,所以能确保一定拿到视图更新后的 DOM。

扩展

到这里,相比大家对 nextTick 都有一定的了解了,那么接下来来看两个在工作中比较罕见,但是又存在的问题吧。

问题一

<div id="root">
  <p id="msg">{{ msg }}</p>
</div>
<script>
	const app = new Vue({
    el: '#root',
    data: { msg: 'ready~~' },
    mounted() {
      this.msg= Math.random()
      console.log('1', this.msg)
      console.log('1 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('2', this.msg)
      console.log('2 innerHTML:' + msg.innerHTML)

			this.$nextTick(() => {
        console.log('nextTick innerHTML:' + msg.innerHTML)
      })

			this.msg= Math.random()
      console.log('3', this.msg)
      console.log('3 innerHTML:' + msg.innerHTML)

    }
	});
</script>

这个问题其实相当简单,因为 nextTick 是异步的,所以这里应该输出和最开始的 demo 是一样的,来验证一下看看吧:

问题二

<div id="root">
  <p id="msg">{{ msg }}</p>
</div>
<script>
	const app = new Vue({
    el: '#root',
    data: { msg: 'ready~~' },
    mounted() {
			this.$nextTick(() => {
        console.log('nextTick innerHTML:' + msg.innerHTML)
      })

      this.msg= Math.random()
      console.log('1', this.msg)
      console.log('1 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('2', this.msg)
      console.log('2 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('3', this.msg)
      console.log('3 innerHTML:' + msg.innerHTML)

    }
	});
</script>

这个问题乍一看仿佛跟上面差不多,但实际上却有本质上的区别。

由于 nextTick 的回调函数入队的时候,值的修改还没有发生,所以输出的应该是上一轮的 DOM 更新后的值,也就是 ready~~。

验证一下看看吧:

问题三

<div id="root">
  <p id="msg">{{ msg }}</p>
</div>
<script>
	const app = new Vue({
    el: '#root',
    data: { msg: 'ready~~' },
    mounted() {


      this.msg= Math.random()
      console.log('1', this.msg)
      console.log('1 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('2', this.msg)
      console.log('2 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('3', this.msg)
      console.log('3 innerHTML:' + msg.innerHTML)

			Promise.resolve().then(() => {
        console.log("promise:", msg.innerHTML);
      });

			this.$nextTick(() => {
        console.log('nextTick innerHTML:' + msg.innerHTML)
      })
    }
	});
</script>

这个问题同样存在着误导。我们知道 vue 异步实际上也是用的 promise,所以会认为这里两个 promise 会依次输出,所以认为答案应该是 promise 输出的值在 nextTick 之前。

但事实上却不是这样,这里 demo 中的 promise 是一个单独的 promise,而 nextTick 回调则是在 callbacks 中的一员,由于前面已经有了修改值得操作,所以 callback 在 promise 之前,所以 nextTick 的输出应该是在 promise 之前。

那么验证一下看看吧:

问题四

<div id="root">
  <p id="msg">{{ msg }}</p>
</div>
<script>
	const app = new Vue({
    el: '#root',
    data: { msg: 'ready~~' },
    mounted() {
			Promise.resolve().then(() => {
        console.log("promise:", msg.innerHTML);
      });

      this.msg= Math.random()
      console.log('1', this.msg)
      console.log('1 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('2', this.msg)
      console.log('2 innerHTML:' + msg.innerHTML)

			this.msg= Math.random()
      console.log('3', this.msg)
      console.log('3 innerHTML:' + msg.innerHTML)

			this.$nextTick(() => {
        console.log('nextTick innerHTML:' + msg.innerHTML)
      })
    }
	});
</script>

这个问题跟上面那道差不多,就不再赘述,直接看答案吧:

总结

可以看出,只要搞清楚了 vue 异步更新策略,以及 nextTick 执行的流程,相关问题就很容易搞明白了。

结语

更佳阅读体验:015 - nextTick 是什么?它是干什么的?实现原理是什么?