Vue nextTick 源码和原理

121 阅读2分钟

Vue nextTick 源码和原理

描述:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

nextTick原理:

异步更新内部是最重要的就是 `nextTick` 方法,它负责将异步任务加入队列和执行异步任务。`Vue` 也将它暴露出来提供给用户使用。在数据修改完成后,立即获取相关DOM还没那么快更新,使用 `nextTick` 便可以解决这一问题。

nextTick 实现原理

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;

nextTick 提供了四种异步方法 Promise.thenMutationObserver、setImmediate、setTimeout(fn,0)

nextTick$nextTick的区别:

他们的区别是 nextTick 在框架内部导出,而 $nextTice 挂载了Vue的原型上, 让每个通过 Vue 构造函数生成的实例都能调用

用法和使用场景:

用法:接受一个回调函数作为参数,它的作用是将回调函数延迟到一次DOM更新之后的操作,如果没有提供回调函数参数且在支持Promise的环境中,nextTick将返回一个Promise。
使用场景:开发过程中,开发者需要在更新完数据之后,需要对新DOM做一些操作,其实我们当时无法对新DOM进行操作,因为这时候还没有重新渲染。

使用:

Y3{XP$O3K0YCR5RKY`1MSF4.png

执行过程:

image.png

源码:

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false
// 执行队列中的每一个回调
function flushCallbacks () {
  pending = false // 重置异步锁
    // 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份并清空回调函数队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
  //执行回调函数队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

/* 优先检测微任务(micro task) */
// 检测浏览器是否原生支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // 如果不支持Promise,则检测是否支持原生的 MutationObserver 
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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
  /* 对于宏任务(macro task) */
// 检测是否支持原生 setImmediate(高版本 IE 和 Edge 支持)
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //宏任务
 
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
  // 以上都不支持的情况下,使用setTimeout
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    //将回调函数推入回调队列
    if (cb) {
      try <img src="{" alt="" width="30%" />
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果异步锁未锁上,锁上异步锁,调用异步函数,准备等同步函数执行完后,就开始执行回调函数队列
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  //如果没有提供回调且支持 Promise 的环境中,则返回一个Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

源码分析:nextTick 函数实现,pending 控制 timerFunc 同一时间只能执行一次

总结:

watch的更新视图操作和`$nextTick(cb)`的回调函数都会放到一个callbacks异步任务队列中,待同步任务执行完成后一并执行;watcher更新视图会创建新的DOM,所以在$nextTick(cb)中可以获取到新的DOM

$nextTick()回调中获取的是内存中的DOM,不关心UI有没有渲染完成