Vue.nextTick 原理与应用

nextTick 的实现原理、降级策略及常见使用场景

问题

Vue.nextTick 的原理是什么?它的 Microtask/Macrotask 降级策略是怎样的?有哪些应用场景?

解答

nextTick 是什么

nextTick 用于在 DOM 更新完成后执行回调。Vue 的数据变化到 DOM 更新是异步的,如果需要操作更新后的 DOM,就要用 nextTick

this.message = 'updated'
// DOM 还没更新
console.log(this.$el.textContent) // 旧值

this.$nextTick(() => {
  // DOM 已更新
  console.log(this.$el.textContent) // 'updated'
})

Vue 2.x 降级策略

Vue 2.x 优先使用微任务,不支持时降级到宏任务:

// Vue 2.x nextTick 简化实现
const callbacks = []
let pending = false

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

// 降级策略
let timerFunc

if (typeof Promise !== 'undefined') {
  // 1. 优先使用 Promise(微任务)
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (typeof MutationObserver !== 'undefined') {
  // 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)
  }
} else if (typeof setImmediate !== 'undefined') {
  // 3. setImmediate(宏任务,仅 IE/Node 支持)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 4. setTimeout(宏任务,兜底方案)
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

function nextTick(cb, ctx) {
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}

Vue 3.x 实现

Vue 3 直接使用 Promise,不再做降级:

// Vue 3.x nextTick 实现
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null

function nextTick(fn) {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(fn) : p
}

应用场景

1. 获取更新后的 DOM

export default {
  data() {
    return { list: [] }
  },
  methods: {
    async addItem() {
      this.list.push({ id: Date.now() })
      
      // 等待 DOM 更新后滚动到底部
      await this.$nextTick()
      const container = this.$refs.container
      container.scrollTop = container.scrollHeight
    }
  }
}

2. created 中操作 DOM

export default {
  created() {
    // created 时 DOM 还不存在
    this.$nextTick(() => {
      // 此时可以访问 DOM
      this.$refs.input.focus()
    })
  }
}

3. 多次数据修改后获取最终 DOM

export default {
  methods: {
    updateMultiple() {
      // 多次修改只会触发一次 DOM 更新
      this.a = 1
      this.b = 2
      this.c = 3
      
      this.$nextTick(() => {
        // 获取所有修改后的 DOM 状态
        console.log(this.$el.innerHTML)
      })
    }
  }
}

4. 配合 v-if 使用

export default {
  data() {
    return { showInput: false }
  },
  methods: {
    show() {
      this.showInput = true
      // v-if 切换后 DOM 才会渲染
      this.$nextTick(() => {
        this.$refs.input.focus()
      })
    }
  }
}

关键点

  • nextTick 将回调延迟到 DOM 更新后执行
  • Vue 2.x 降级顺序:Promise → MutationObserver → setImmediate → setTimeout
  • Vue 3.x 直接使用 Promise,不再降级
  • 微任务比宏任务执行时机更早,能更快响应
  • 同一事件循环内多次调用 nextTick,回调会合并到一个队列中批量执行