Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue 的 nextTick 原理 #14

Open
yangrenmu opened this issue Sep 24, 2019 · 0 comments
Open

vue 的 nextTick 原理 #14

yangrenmu opened this issue Sep 24, 2019 · 0 comments
Labels

Comments

@yangrenmu
Copy link
Owner

用法

nextTick 是 vue 中的一个重要的API,先看下官方文档的介绍。

vue.nextTick( [callback, context] )

  • 参数:

    • {Function} [callback]
    • {Object} [context]
  • 用法:

    在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
    // 修改数据
    vm.msg = 'Hello'
    // DOM 还没有更新
    Vue.nextTick(function () {
      // DOM 更新了
    })
    
    // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
    Vue.nextTick()
      .then(function () {
        // DOM 更新了
      })

可以看出,nextTick 实际上就是 DOM 更新后的一个回调。那DOM更新的时机是什么时候呢?

DOM 的更新时机

这要从 js 的运行机制说起。(硬广:js 的运行机制)。简单点说就是,js 代码的执行顺序是基于事件循环的,大致分为几个步骤:

  • 1、同步任务都在主线程上执行,形成一个执行栈。
  • 2、异步任务会被放入主线程之外的任务队列中。
  • 3、主线程同步任务执行完后,会去任务队列中查找异步任务,将其放到执行栈中执行。
  • 4、主线程不断重复上面三步,这也就是常说的事件循环。
    当然异步任务分为宏任务和微任务。具体就不说了,可以看下硬广。主线程在同步任务执行完后,会首先从任务队列中查找并执行微任务,直到最后一个微任务执行完,第一次事件循环结束。
    开始第二次事件循环,任务队列中第一个宏任务变成同步任务,被首先执行,然后执行微任务,不断这样循环执行,直到所有任务执行完毕。
    而 DOM 的更新时机则是在微任务执行结束后。我们看个栗子:
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .box {
      width: 400px;
      height: 200px;
      margin: 0 auto;
      display: flex;
      justify-content: center;
      align-items: center;
      border: 1px solid #259;
    }
  </style>
</head>

<body>
  <div class="box">
    <input class="input" type="text">
  </div>
</body>

<script>
  const box = document.querySelector('.box')
  function sleep(time) {
    const start = new Date().getTime();
    while (new Date().getTime() - start < time) { }
  }

  // click监听事件
  function onClick() {
    const testElement = document.querySelector('.input') || ''
    testElement.focus()
    Promise.resolve().then(function () {
      console.log('promise')
      sleep(2000)
    })
    setTimeout(() => {
      sleep(2000)
      console.log('timeout')
    })
  }

  box.addEventListener('click', onClick)
</script>

</html>


我们可以看到,DOM 的渲染是在微任务之后的,而且在下一次事件循环之前。

nextTick 源码解析

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

// 空函数
import { noop } from 'shared/util'
// 处理错误的函数
import { handleError } from './error'
// 判断运行环境
import { isIE, isIOS, isNative } from './env'
// 是否使用微任务的标识符
export let isUsingMicroTask = false
// 存放回调函数的数组
const callbacks = []
// nexttick 执行状态
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]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 如果浏览器支持原生 promise, 则直接使用 promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  // 将是否使用微任务改为 true
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  // MutationObserver:如果浏览器支持 MutationObserver,则使用 MutationObserver
  //(该 API 是个微任务,可以监听 DOM 元素是否变动,当所有 DOM 操作完成后,会触发相应的事件)
  // 当 不支持 promise 时,优先使用该 API
  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)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // setImmediate:宏任务,只支持 IE、edge 浏览器。
  // 与 setTimeout 相比,优势在于它是立即执行,而 setTimeout 最小间隔是 4ms
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 封装的nextTick函数,cb:回调函数,ctx:this 指向
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // 回调函数统一进入 callbacks 中进行处理
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 如果没有传入回调函数,且当前环境支持 promise ,则返回一个 promise 对象
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
@yangrenmu yangrenmu added the vue label Sep 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant