likes
comments
collection
share

04 | 【阅读Vue2源码】$nextTick实现原理

作者站长头像
站长
· 阅读数 14

前言

$nextTick是个很常用的API,简单来讲其作用是让函数延后执行。

来看下官方的描述

04 | 【阅读Vue2源码】$nextTick实现原理

深入响应式原理的文章中也有介绍到

04 | 【阅读Vue2源码】$nextTick实现原理

上面官方的描述,其实也解答了,nextTick就是使用Promise、setTimeout等异步函数实现的。

分析

基本用法

<section id="app">
  <div id="count">{{ count }}</div>
  <button @click="plus">+1</button>
</section>

new Vue({
  name: 'SimpleDemoAPI',
  data() {
    return {
      count: 0
    }
  },
  methods: {
    plus() {
      this.count += 1;

      // 未更新前的值
      console.log('alan->count sync', document.getElementById('count').innerText) 
      
      this.$nextTick(() => {
        // 更新后的值
        console.log('alan->count $nextTick', document.getElementById('count').innerText) 
      })
    }
  }
})

04 | 【阅读Vue2源码】$nextTick实现原理

实现原理

源码分析

$nextTick实际上是nextTick函数,在初始化Vue的时候,把nextTick赋值给了$nextTick

// src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this)
}

源码位置:src/core/util/next-tick.js

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
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 把回调队列中的函数取出来执行
  }
}

// 定义定时器函数,后面赋值
let timerFunc

// 如果运行环境支持Promise,则使用Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => { // 定义行数,赋值给timerFunc
    p.then(flushCallbacks) // 将flushCallbacks作为resolve的回调函数,执行完回调队列中的函数

    // ios中有特殊情况
    // 在有问题的UIWebViews中,Promise.then不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。
    // 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true // 标记使用微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 当运行环境不支持Promise时,判断下是否支持MutationObserver,如支持则使用MutationObserver
  let counter = 1
  // 用flushCallbacks作为MutationObserver的回调函数
  const observer = new MutationObserver(flushCallbacks)
  // 创建临时的文本节点,用MutationObserver观测它的变化,以触发new MutationObserver(flushCallbacks)执行
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => { // 将更改DOM的函数赋值给timerFunc
    // 当执行nextTick时,会执行timerFunc,这里改变textNode的值,每次+1
    // 触发new MutationObserver(flushCallbacks)执行
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 当运行环境不支持Promise、MutationObserver时,判断下是否支持setImmediate,如支持则使用setImmediate
  // setImmediate这个API只在node环境下可用
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 如果上面的方式都不行,则使用setTimeout
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 调用$nextTick时,执行该函数
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve // 缓存Promise的resolve
  // 收集回调函数
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      // 执行Promise的resolve回调
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 执行定时器函数,核心逻辑
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

实现逻辑:

  1. 初始化模块文件

  2. 定义callbacks

  3. 定义状态pending = false

  4. 定义冲刷队列函数flushCallbacks()

  5. 定义定时器函数timerFunc()

    • 如果有Promise,直接执行Promise.resolve(flushCallbacks)
    • 如果有MutationObserver,就new MutationObserver(flushCallbacks),然后创建一个文本节点,用observer观测它。在timerFunc()里改变文本节点的值textNode.data = String((counter + 1) % 2),当执行timerFunc()时,改变文本节点,变化一次就触发一次MutationObserver,就会执行flushCallbacks()
    • 如果有setImmediate,就执行setImmediate(flushCallbacks)
    • 如果上面都不行,就使用setTimeout,执行setTimeout(flushCallbacks, 0)
  6. 把回调函数cb,放入一个callbacks队列里

  7. 标记执行状态pending = true

  8. 执行定时器函数timerFunc(),走timerFunc里面的逻辑

调用链路

04 | 【阅读Vue2源码】$nextTick实现原理

一些疑问

ios下为什么要执行一下setTimeout?

在有问题的UIWebViews中,Promise.then不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。因此,我们可以通过添加空计时器来“强制”刷新微任务队列。

为什么要用MutationObserver呢?

在不支持Promise的地方使用,例如:PhantomJS, iOS7, Android 4.4

降级到定时器,为什么优先选择setImmediate?

从技术上讲,它利用了(宏)任务队列,但它仍然是比setTimeout更好的选择。

总结

nextTick其实挺简单的,底层就是使用了微任务/宏任务来实现,Promise -> MutationObserver -> setImmediate -> setTimeout,将回调函数存放到队列中,然后利用事件循环的特点,每次循环结束前,先将微任务、宏任务清空。