Skip to content

nextTick

1.概念

这是一个生命周期钩子,主要作用是在数据更新后获取最新的dom

nextTick是一个函数,挂载在vm实例上,可以通过this.$nextTick()调用,并且接受一个回调函数作为参数

js
nextTick(callback,context)
  • 第一个参数为:回调函数(可以获取最近的DOM结构)
  • 第二个参数为:执行函数上下文

2.使用

有如下例子,一个div.box中应用了data中的一个数据

定义了一个改变数据的方法change(),改变数据后获取box中的innerHtml

vue
<template>
	<div>
		<div class="box" ref="box">{{ value }}</div>
		<button @click="change">click</button>
	</div>
</template>
<script>
export default {
	name: 'App',
	data() {
		return {
			value: 11,
		}
	},
	methods: {
		change() {
			this.value = 22
			console.log(this.$refs.box.innerHTML)
		},
	},
}
</script>

发现第一次点击,虽然视图更新了,但打印的仍然是旧数据

第二次点击,由于第一次点击视图上的数据已经是22了,才会打印22

PixPin_2024-10-21_11-02-45

如此类型的操作,就没有获取到最新的dom

而使用nextTick,则解决此问题

vue
<template>
	<div>
		<div class="box" ref="box">{{ value }}</div>
		<button @click="change">click</button>
	</div>
</template>
<script>
export default {
	name: 'App',
	data() {
		return {
			value: 11,
		}
	},
	methods: {
		change() {
			this.value = 22
			this.$nextTick(() => {
				console.log(this.$refs.box.innerHTML)
			})
		},
	},
}
</script>

可以看到点击一次,就获取到了最新dom中的数据

PixPin_2024-10-21_11-05-21

3.原理

在Vue中,dom更新是异步的,当改变了一个数据导致dom发生改变,而同时我们在同步代码中获取这个dom,获取的是旧的dom,数据也会是旧的

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

源码大致如下

callbacks也就是异步操作队列

callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次

js
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;

  // cb 回调函数会经统一处理压入 callbacks 数组
  callbacks.push(() => {
    if (cb) {
      // 给 cb 回调函数执行加上了 try-catch 错误处理
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });

  // 执行异步延迟函数 timerFunc
  if (!pending) {
    pending = true;
    timerFunc();
  }

  // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve;
    });
  }
}

timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:Promise.thenMutationObserversetImmediatesetTimeout

通过上面任意一种方法,进行降级操作

js
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判断1:是否原生支持Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //判断2:是否原生支持MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //判断3:是否原生支持setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //判断4:上面都不行,直接用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

MIT Licensed