Vue的$nextTick的实现方式

Vue 的 $nextTick 是实现异步更新的核心 API,其实现方式经历了演变:

1. 实现原理

$nextTick 的本质是将回调函数延迟到下一个 DOM 更新周期之后执行。

2. 实现方式演变

2.1 优先使用微任务 (Microtask)

// Vue 2.5+ 的实现
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

// 优先级:Promise > MutationObserver > setImmediate > setTimeout
if (typeof Promise !== 'undefined') {
  // 使用 Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (typeof MutationObserver !== 'undefined') {
  // 使用 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') {
  // 使用 setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 降级到 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(cb, ctx) {
  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()
  }
  
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

2.2 Vue 实例方法

Vue.prototype.$nextTick = function(fn) {
  return nextTick(fn, this)
}

3. 使用方式

3.1 回调函数方式

// 方式1:回调函数
this.message = 'Hello'
this.$nextTick(function() {
  // DOM 更新完成
  console.log(this.$el.textContent) // 输出: Hello
})

3.2 Promise 方式

// 方式2:Promise (Vue 2.1.0+)
this.message = 'Hello'
this.$nextTick().then(() => {
  // DOM 更新完成
  console.log('DOM updated')
})

// 方式3:async/await
async updateMessage() {
  this.message = 'Hello'
  await this.$nextTick()
  // DOM 更新完成
  console.log('DOM updated')
}

4. 执行时机

4.1 同步代码 vs 异步更新

export default {
  data() {
    return {
      message: 'Hello',
      count: 0
    }
  },
  methods: {
    updateData() {
      // 同步代码
      this.message = 'World'
      this.count++
      
      console.log('同步:', this.message) // 输出: World
      console.log('同步:', this.count)   // 输出: 1
      
      // DOM 还未更新
      console.log('DOM未更新:', this.$el.textContent) // 可能还是旧值
      
      this.$nextTick(() => {
        // DOM 已更新
        console.log('DOM已更新:', this.$el.textContent) // 新值
        console.log('nextTick:', this.count) // 1
      })
    }
  }
}

5. 与事件循环的关系

// 执行顺序示例
console.log('1. 同步任务开始')

this.message = 'Hello'

// 微任务
Promise.resolve().then(() => {
  console.log('3. Promise 微任务')
})

// nextTick 微任务
this.$nextTick(() => {
  console.log('4. nextTick 微任务')
})

// 宏任务
setTimeout(() => {
  console.log('6. setTimeout 宏任务')
}, 0)

console.log('2. 同步任务结束')

// 执行顺序:
// 1. 同步任务开始
// 2. 同步任务结束  
// 3. Promise 微任务
// 4. nextTick 微任务
// 5. DOM 更新 (浏览器渲染)
// 6. setTimeout 宏任务

6. 实际应用场景

6.1 获取更新后的 DOM

methods: {
  showInput() {
    this.show = true
    this.$nextTick(() => {
      this.$refs.input.focus() // 确保 input 已渲染
    })
  }
}

6.2 基于更新后 DOM 的计算

methods: {
  updateList() {
    this.items.push('new item')
    this.$nextTick(() => {
      // 确保列表已更新
      const lastItem = this.$el.querySelector('.item:last-child')
      lastItem.scrollIntoView()
    })
  }
}

6.3 在 watcher 中使用

watch: {
  data() {
    // 数据变化时,DOM 还未更新
    console.log('数据变化')
    
    this.$nextTick(() => {
      // DOM 已更新
      console.log('DOM 更新完成')
    })
  }
}

总结

Vue 的 $nextTick 实现特点:

  • 优先级:微任务 > 宏任务,确保在 DOM 更新后执行
  • 兼容性:降级策略保证在各种环境下的可用性
  • 灵活性:支持回调和 Promise 两种使用方式
  • 批处理:同一事件循环中的多个 nextTick 会被合并执行

这种实现确保了在数据变化后,能够可靠地在 DOM 更新完成后执行回调函数。

posted @ 2025-10-13 16:27  阿木隆1237  阅读(11)  评论(0)    收藏  举报